From 5e043981bb745c23001c3b722fb0cb8345f71529 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 8 May 2025 15:10:42 +0000 Subject: [PATCH 001/448] initial commit --- .devcontainer/Dockerfile | 9 + .devcontainer/devcontainer.json | 43 + .github/workflows/ci.yml | 52 + .gitignore | 16 + .python-version | 1 + .stats.yml | 4 + Brewfile | 2 + CONTRIBUTING.md | 129 ++ LICENSE | 201 ++ README.md | 382 +++- SECURITY.md | 23 + api.md | 25 + bin/publish-pypi | 6 + examples/.keep | 4 + mypy.ini | 50 + noxfile.py | 9 + pyproject.toml | 207 ++ requirements-dev.lock | 104 + requirements.lock | 45 + scripts/bootstrap | 19 + scripts/format | 8 + scripts/lint | 11 + scripts/mock | 41 + scripts/test | 61 + scripts/utils/ruffen-docs.py | 167 ++ src/kernel/__init__.py | 84 + src/kernel/_base_client.py | 1943 +++++++++++++++++ src/kernel/_client.py | 402 ++++ src/kernel/_compat.py | 219 ++ src/kernel/_constants.py | 14 + src/kernel/_exceptions.py | 108 + src/kernel/_files.py | 123 ++ src/kernel/_models.py | 803 +++++++ src/kernel/_qs.py | 150 ++ src/kernel/_resource.py | 43 + src/kernel/_response.py | 830 +++++++ src/kernel/_streaming.py | 333 +++ src/kernel/_types.py | 217 ++ src/kernel/_utils/__init__.py | 57 + src/kernel/_utils/_logs.py | 25 + src/kernel/_utils/_proxy.py | 65 + src/kernel/_utils/_reflection.py | 42 + src/kernel/_utils/_streams.py | 12 + src/kernel/_utils/_sync.py | 86 + src/kernel/_utils/_transform.py | 447 ++++ src/kernel/_utils/_typing.py | 151 ++ src/kernel/_utils/_utils.py | 422 ++++ src/kernel/_version.py | 4 + src/kernel/lib/.keep | 4 + src/kernel/py.typed | 0 src/kernel/resources/__init__.py | 33 + src/kernel/resources/apps.py | 401 ++++ src/kernel/resources/browser.py | 135 ++ src/kernel/types/__init__.py | 10 + src/kernel/types/app_deploy_params.py | 24 + src/kernel/types/app_deploy_response.py | 16 + src/kernel/types/app_invoke_params.py | 20 + src/kernel/types/app_invoke_response.py | 13 + .../types/app_retrieve_invocation_response.py | 25 + .../types/browser_create_session_response.py | 18 + tests/__init__.py | 1 + tests/api_resources/__init__.py | 1 + tests/api_resources/test_apps.py | 292 +++ tests/api_resources/test_browser.py | 78 + tests/conftest.py | 51 + tests/sample_file.txt | 1 + tests/test_client.py | 1680 ++++++++++++++ tests/test_deepcopy.py | 58 + tests/test_extract_files.py | 64 + tests/test_files.py | 51 + tests/test_models.py | 891 ++++++++ tests/test_qs.py | 78 + tests/test_required_args.py | 111 + tests/test_response.py | 277 +++ tests/test_streaming.py | 248 +++ tests/test_transform.py | 453 ++++ tests/test_utils/test_proxy.py | 34 + tests/test_utils/test_typing.py | 73 + tests/utils.py | 159 ++ 79 files changed, 13498 insertions(+), 1 deletion(-) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .python-version create mode 100644 .stats.yml create mode 100644 Brewfile create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 SECURITY.md create mode 100644 api.md create mode 100644 bin/publish-pypi create mode 100644 examples/.keep create mode 100644 mypy.ini create mode 100644 noxfile.py create mode 100644 pyproject.toml create mode 100644 requirements-dev.lock create mode 100644 requirements.lock create mode 100755 scripts/bootstrap create mode 100755 scripts/format create mode 100755 scripts/lint create mode 100755 scripts/mock create mode 100755 scripts/test create mode 100644 scripts/utils/ruffen-docs.py create mode 100644 src/kernel/__init__.py create mode 100644 src/kernel/_base_client.py create mode 100644 src/kernel/_client.py create mode 100644 src/kernel/_compat.py create mode 100644 src/kernel/_constants.py create mode 100644 src/kernel/_exceptions.py create mode 100644 src/kernel/_files.py create mode 100644 src/kernel/_models.py create mode 100644 src/kernel/_qs.py create mode 100644 src/kernel/_resource.py create mode 100644 src/kernel/_response.py create mode 100644 src/kernel/_streaming.py create mode 100644 src/kernel/_types.py create mode 100644 src/kernel/_utils/__init__.py create mode 100644 src/kernel/_utils/_logs.py create mode 100644 src/kernel/_utils/_proxy.py create mode 100644 src/kernel/_utils/_reflection.py create mode 100644 src/kernel/_utils/_streams.py create mode 100644 src/kernel/_utils/_sync.py create mode 100644 src/kernel/_utils/_transform.py create mode 100644 src/kernel/_utils/_typing.py create mode 100644 src/kernel/_utils/_utils.py create mode 100644 src/kernel/_version.py create mode 100644 src/kernel/lib/.keep create mode 100644 src/kernel/py.typed create mode 100644 src/kernel/resources/__init__.py create mode 100644 src/kernel/resources/apps.py create mode 100644 src/kernel/resources/browser.py create mode 100644 src/kernel/types/__init__.py create mode 100644 src/kernel/types/app_deploy_params.py create mode 100644 src/kernel/types/app_deploy_response.py create mode 100644 src/kernel/types/app_invoke_params.py create mode 100644 src/kernel/types/app_invoke_response.py create mode 100644 src/kernel/types/app_retrieve_invocation_response.py create mode 100644 src/kernel/types/browser_create_session_response.py create mode 100644 tests/__init__.py create mode 100644 tests/api_resources/__init__.py create mode 100644 tests/api_resources/test_apps.py create mode 100644 tests/api_resources/test_browser.py create mode 100644 tests/conftest.py create mode 100644 tests/sample_file.txt create mode 100644 tests/test_client.py create mode 100644 tests/test_deepcopy.py create mode 100644 tests/test_extract_files.py create mode 100644 tests/test_files.py create mode 100644 tests/test_models.py create mode 100644 tests/test_qs.py create mode 100644 tests/test_required_args.py create mode 100644 tests/test_response.py create mode 100644 tests/test_streaming.py create mode 100644 tests/test_transform.py create mode 100644 tests/test_utils/test_proxy.py create mode 100644 tests/test_utils/test_typing.py create mode 100644 tests/utils.py diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..ff261bad --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,9 @@ +ARG VARIANT="3.9" +FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} + +USER vscode + +RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.44.0" RYE_INSTALL_OPTION="--yes" bash +ENV PATH=/home/vscode/.rye/shims:$PATH + +RUN echo "[[ -d .venv ]] && source .venv/bin/activate || export PATH=\$PATH" >> /home/vscode/.bashrc diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..c17fdc16 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,43 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/debian +{ + "name": "Debian", + "build": { + "dockerfile": "Dockerfile", + "context": ".." + }, + + "postStartCommand": "rye sync --all-features", + + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python" + ], + "settings": { + "terminal.integrated.shell.linux": "/bin/bash", + "python.pythonPath": ".venv/bin/python", + "python.defaultInterpreterPath": ".venv/bin/python", + "python.typeChecking": "basic", + "terminal.integrated.env.linux": { + "PATH": "/home/vscode/.rye/shims:${env:PATH}" + } + } + } + }, + "features": { + "ghcr.io/devcontainers/features/node:1": {} + } + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..0d9000de --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,52 @@ +name: CI +on: + push: + branches-ignore: + - 'generated' + - 'codegen/**' + - 'integrated/**' + - 'stl-preview-head/**' + - 'stl-preview-base/**' + +jobs: + lint: + timeout-minutes: 10 + name: lint + runs-on: ${{ github.repository == 'stainless-sdks/kernel-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + steps: + - uses: actions/checkout@v4 + + - name: Install Rye + run: | + curl -sSf https://rye.astral.sh/get | bash + echo "$HOME/.rye/shims" >> $GITHUB_PATH + env: + RYE_VERSION: '0.44.0' + RYE_INSTALL_OPTION: '--yes' + + - name: Install dependencies + run: rye sync --all-features + + - name: Run lints + run: ./scripts/lint + + test: + timeout-minutes: 10 + name: test + runs-on: ${{ github.repository == 'stainless-sdks/kernel-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + steps: + - uses: actions/checkout@v4 + + - name: Install Rye + run: | + curl -sSf https://rye.astral.sh/get | bash + echo "$HOME/.rye/shims" >> $GITHUB_PATH + env: + RYE_VERSION: '0.44.0' + RYE_INSTALL_OPTION: '--yes' + + - name: Bootstrap + run: ./scripts/bootstrap + + - name: Run tests + run: ./scripts/test diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..87797408 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +.prism.log +.vscode +_dev + +__pycache__ +.mypy_cache + +dist + +.venv +.idea + +.env +.envrc +codegen.log +Brewfile.lock.json diff --git a/.python-version b/.python-version new file mode 100644 index 00000000..43077b24 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.9.18 diff --git a/.stats.yml b/.stats.yml new file mode 100644 index 00000000..7bdb6b3d --- /dev/null +++ b/.stats.yml @@ -0,0 +1,4 @@ +configured_endpoints: 4 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2a9a9232d72f52fc9b2404958857a1b744762356421cf1bffb99db2c24045378.yml +openapi_spec_hash: ba3c6823319d99762e7e1c6a624bc2ed +config_hash: e7de12a0c945ca8d537120d0d3b484b2 diff --git a/Brewfile b/Brewfile new file mode 100644 index 00000000..492ca37b --- /dev/null +++ b/Brewfile @@ -0,0 +1,2 @@ +brew "rye" + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..24d5b0aa --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,129 @@ +## Setting up the environment + +### With Rye + +We use [Rye](https://rye.astral.sh/) to manage dependencies because it will automatically provision a Python environment with the expected Python version. To set it up, run: + +```sh +$ ./scripts/bootstrap +``` + +Or [install Rye manually](https://rye.astral.sh/guide/installation/) and run: + +```sh +$ rye sync --all-features +``` + +You can then run scripts using `rye run python script.py` or by activating the virtual environment: + +```sh +$ rye shell +# or manually activate - https://docs.python.org/3/library/venv.html#how-venvs-work +$ source .venv/bin/activate + +# now you can omit the `rye run` prefix +$ python script.py +``` + +### Without Rye + +Alternatively if you don't want to install `Rye`, you can stick with the standard `pip` setup by ensuring you have the Python version specified in `.python-version`, create a virtual environment however you desire and then install dependencies using this command: + +```sh +$ pip install -r requirements-dev.lock +``` + +## Modifying/Adding code + +Most of the SDK is generated code. Modifications to code will be persisted between generations, but may +result in merge conflicts between manual patches and changes from the generator. The generator will never +modify the contents of the `src/kernel/lib/` and `examples/` directories. + +## Adding and running examples + +All files in the `examples/` directory are not modified by the generator and can be freely edited or added to. + +```py +# add an example to examples/.py + +#!/usr/bin/env -S rye run python +… +``` + +```sh +$ chmod +x examples/.py +# run the example against your api +$ ./examples/.py +``` + +## Using the repository from source + +If you’d like to use the repository from source, you can either install from git or link to a cloned repository: + +To install via git: + +```sh +$ pip install git+ssh://git@github.com/stainless-sdks/kernel-python.git +``` + +Alternatively, you can build from source and install the wheel file: + +Building this package will create two files in the `dist/` directory, a `.tar.gz` containing the source files and a `.whl` that can be used to install the package efficiently. + +To create a distributable version of the library, all you have to do is run this command: + +```sh +$ rye build +# or +$ python -m build +``` + +Then to install: + +```sh +$ pip install ./path-to-wheel-file.whl +``` + +## Running tests + +Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests. + +```sh +# you will need npm installed +$ npx prism mock path/to/your/openapi.yml +``` + +```sh +$ ./scripts/test +``` + +## Linting and formatting + +This repository uses [ruff](https://github.com/astral-sh/ruff) and +[black](https://github.com/psf/black) to format the code in the repository. + +To lint: + +```sh +$ ./scripts/lint +``` + +To format and fix all ruff issues automatically: + +```sh +$ ./scripts/format +``` + +## Publishing and releases + +Changes made to this repository via the automated release PR pipeline should publish to PyPI automatically. If +the changes aren't made through the automated pipeline, you may want to make releases manually. + +### Publish with a GitHub workflow + +You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/stainless-sdks/kernel-python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. + +### Publish manually + +If you need to manually release a package, you can run the `bin/publish-pypi` script with a `PYPI_TOKEN` set on +the environment. diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..b32a077a --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 Kernel + + 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 6866e5bf..06527eb3 100644 --- a/README.md +++ b/README.md @@ -1 +1,381 @@ -# kernel-python \ No newline at end of file +# Kernel Python API library + +[![PyPI version](https://img.shields.io/pypi/v/kernel.svg)](https://pypi.org/project/kernel/) + +The Kernel Python library provides convenient access to the Kernel REST API from any Python 3.8+ +application. The library includes type definitions for all request params and response fields, +and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx). + +It is generated with [Stainless](https://www.stainless.com/). + +## Documentation + +The full API of this library can be found in [api.md](api.md). + +## Installation + +```sh +# install from this staging repo +pip install git+ssh://git@github.com/stainless-sdks/kernel-python.git +``` + +> [!NOTE] +> Once this package is [published to PyPI](https://app.stainless.com/docs/guides/publish), this will become: `pip install --pre kernel` + +## Usage + +The full API of this library can be found in [api.md](api.md). + +```python +import os +from kernel import Kernel + +client = Kernel( + api_key=os.environ.get("KERNEL_API_KEY"), # This is the default and can be omitted +) + +response = client.apps.deploy( + app_name="REPLACE_ME", + file=b"REPLACE_ME", + version="REPLACE_ME", +) +print(response.id) +``` + +While you can provide an `api_key` keyword argument, +we recommend using [python-dotenv](https://pypi.org/project/python-dotenv/) +to add `KERNEL_API_KEY="My API Key"` to your `.env` file +so that your API Key is not stored in source control. + +## Async usage + +Simply import `AsyncKernel` instead of `Kernel` and use `await` with each API call: + +```python +import os +import asyncio +from kernel import AsyncKernel + +client = AsyncKernel( + api_key=os.environ.get("KERNEL_API_KEY"), # This is the default and can be omitted +) + + +async def main() -> None: + response = await client.apps.deploy( + app_name="REPLACE_ME", + file=b"REPLACE_ME", + version="REPLACE_ME", + ) + print(response.id) + + +asyncio.run(main()) +``` + +Functionality between the synchronous and asynchronous clients is otherwise identical. + +## Using types + +Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev) which also provide helper methods for things like: + +- Serializing back into JSON, `model.to_json()` +- Converting to a dictionary, `model.to_dict()` + +Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. + +## File uploads + +Request parameters that correspond to file uploads can be passed as `bytes`, or a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance or a tuple of `(filename, contents, media type)`. + +```python +from pathlib import Path +from kernel import Kernel + +client = Kernel() + +client.apps.deploy( + app_name="my-awesome-app", + file=Path("/path/to/file"), + version="1.0.0", +) +``` + +The async client uses the exact same interface. If you pass a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance, the file contents will be read asynchronously automatically. + +## Handling errors + +When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `kernel.APIConnectionError` is raised. + +When the API returns a non-success status code (that is, 4xx or 5xx +response), a subclass of `kernel.APIStatusError` is raised, containing `status_code` and `response` properties. + +All errors inherit from `kernel.APIError`. + +```python +import kernel +from kernel import Kernel + +client = Kernel() + +try: + client.apps.deploy( + app_name="REPLACE_ME", + file=b"REPLACE_ME", + version="REPLACE_ME", + ) +except kernel.APIConnectionError as e: + print("The server could not be reached") + print(e.__cause__) # an underlying Exception, likely raised within httpx. +except kernel.RateLimitError as e: + print("A 429 status code was received; we should back off a bit.") +except kernel.APIStatusError as e: + print("Another non-200-range status code was received") + print(e.status_code) + print(e.response) +``` + +Error codes are as follows: + +| Status Code | Error Type | +| ----------- | -------------------------- | +| 400 | `BadRequestError` | +| 401 | `AuthenticationError` | +| 403 | `PermissionDeniedError` | +| 404 | `NotFoundError` | +| 422 | `UnprocessableEntityError` | +| 429 | `RateLimitError` | +| >=500 | `InternalServerError` | +| N/A | `APIConnectionError` | + +### Retries + +Certain errors are automatically retried 2 times by default, with a short exponential backoff. +Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict, +429 Rate Limit, and >=500 Internal errors are all retried by default. + +You can use the `max_retries` option to configure or disable retry settings: + +```python +from kernel import Kernel + +# Configure the default for all requests: +client = Kernel( + # default is 2 + max_retries=0, +) + +# Or, configure per-request: +client.with_options(max_retries=5).apps.deploy( + app_name="REPLACE_ME", + file=b"REPLACE_ME", + version="REPLACE_ME", +) +``` + +### Timeouts + +By default requests time out after 1 minute. You can configure this with a `timeout` option, +which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/#fine-tuning-the-configuration) object: + +```python +from kernel import Kernel + +# Configure the default for all requests: +client = Kernel( + # 20 seconds (default is 1 minute) + timeout=20.0, +) + +# More granular control: +client = Kernel( + timeout=httpx.Timeout(60.0, read=5.0, write=10.0, connect=2.0), +) + +# Override per-request: +client.with_options(timeout=5.0).apps.deploy( + app_name="REPLACE_ME", + file=b"REPLACE_ME", + version="REPLACE_ME", +) +``` + +On timeout, an `APITimeoutError` is thrown. + +Note that requests that time out are [retried twice by default](#retries). + +## Advanced + +### Logging + +We use the standard library [`logging`](https://docs.python.org/3/library/logging.html) module. + +You can enable logging by setting the environment variable `KERNEL_LOG` to `info`. + +```shell +$ export KERNEL_LOG=info +``` + +Or to `debug` for more verbose logging. + +### How to tell whether `None` means `null` or missing + +In an API response, a field may be explicitly `null`, or missing entirely; in either case, its value is `None` in this library. You can differentiate the two cases with `.model_fields_set`: + +```py +if response.my_field is None: + if 'my_field' not in response.model_fields_set: + print('Got json like {}, without a "my_field" key present at all.') + else: + print('Got json like {"my_field": null}.') +``` + +### Accessing raw response data (e.g. headers) + +The "raw" Response object can be accessed by prefixing `.with_raw_response.` to any HTTP method call, e.g., + +```py +from kernel import Kernel + +client = Kernel() +response = client.apps.with_raw_response.deploy( + app_name="REPLACE_ME", + file=b"REPLACE_ME", + version="REPLACE_ME", +) +print(response.headers.get('X-My-Header')) + +app = response.parse() # get the object that `apps.deploy()` would have returned +print(app.id) +``` + +These methods return an [`APIResponse`](https://github.com/stainless-sdks/kernel-python/tree/main/src/kernel/_response.py) object. + +The async client returns an [`AsyncAPIResponse`](https://github.com/stainless-sdks/kernel-python/tree/main/src/kernel/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. + +#### `.with_streaming_response` + +The above interface eagerly reads the full response body when you make the request, which may not always be what you want. + +To stream the response body, use `.with_streaming_response` instead, which requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`. In the async client, these are async methods. + +```python +with client.apps.with_streaming_response.deploy( + app_name="REPLACE_ME", + file=b"REPLACE_ME", + version="REPLACE_ME", +) as response: + print(response.headers.get("X-My-Header")) + + for line in response.iter_lines(): + print(line) +``` + +The context manager is required so that the response will reliably be closed. + +### Making custom/undocumented requests + +This library is typed for convenient access to the documented API. + +If you need to access undocumented endpoints, params, or response properties, the library can still be used. + +#### Undocumented endpoints + +To make requests to undocumented endpoints, you can make requests using `client.get`, `client.post`, and other +http verbs. Options on the client will be respected (such as retries) when making this request. + +```py +import httpx + +response = client.post( + "/foo", + cast_to=httpx.Response, + body={"my_param": True}, +) + +print(response.headers.get("x-foo")) +``` + +#### Undocumented request params + +If you want to explicitly send an extra param, you can do so with the `extra_query`, `extra_body`, and `extra_headers` request +options. + +#### Undocumented response properties + +To access undocumented response properties, you can access the extra fields like `response.unknown_prop`. You +can also get all the extra fields on the Pydantic model as a dict with +[`response.model_extra`](https://docs.pydantic.dev/latest/api/base_model/#pydantic.BaseModel.model_extra). + +### Configuring the HTTP client + +You can directly override the [httpx client](https://www.python-httpx.org/api/#client) to customize it for your use case, including: + +- Support for [proxies](https://www.python-httpx.org/advanced/proxies/) +- Custom [transports](https://www.python-httpx.org/advanced/transports/) +- Additional [advanced](https://www.python-httpx.org/advanced/clients/) functionality + +```python +import httpx +from kernel import Kernel, DefaultHttpxClient + +client = Kernel( + # Or use the `KERNEL_BASE_URL` env var + base_url="http://my.test.server.example.com:8083", + http_client=DefaultHttpxClient( + proxy="http://my.test.proxy.example.com", + transport=httpx.HTTPTransport(local_address="0.0.0.0"), + ), +) +``` + +You can also customize the client on a per-request basis by using `with_options()`: + +```python +client.with_options(http_client=DefaultHttpxClient(...)) +``` + +### Managing HTTP resources + +By default the library closes underlying HTTP connections whenever the client is [garbage collected](https://docs.python.org/3/reference/datamodel.html#object.__del__). You can manually close the client using the `.close()` method if desired, or with a context manager that closes when exiting. + +```py +from kernel import Kernel + +with Kernel() as client: + # make requests here + ... + +# HTTP client is now closed +``` + +## Versioning + +This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions: + +1. Changes that only affect static types, without breaking runtime behavior. +2. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_ +3. Changes that we do not expect to impact the vast majority of users in practice. + +We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. + +We are keen for your feedback; please open an [issue](https://www.github.com/stainless-sdks/kernel-python/issues) with questions, bugs, or suggestions. + +### Determining the installed version + +If you've upgraded to the latest version but aren't seeing any new features you were expecting then your python environment is likely still using an older version. + +You can determine the version that is being used at runtime with: + +```py +import kernel +print(kernel.__version__) +``` + +## Requirements + +Python 3.8 or higher. + +## Contributing + +See [the contributing documentation](./CONTRIBUTING.md). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..bd2ba47d --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +## Reporting Security Issues + +This SDK is generated by [Stainless Software Inc](http://stainless.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken. + +To report a security issue, please contact the Stainless team at security@stainless.com. + +## Responsible Disclosure + +We appreciate the efforts of security researchers and individuals who help us maintain the security of +SDKs we generate. If you believe you have found a security vulnerability, please adhere to responsible +disclosure practices by allowing us a reasonable amount of time to investigate and address the issue +before making any information public. + +## Reporting Non-SDK Related Security Issues + +If you encounter security issues that are not directly related to SDKs but pertain to the services +or products provided by Kernel please follow the respective company's security reporting guidelines. + +--- + +Thank you for helping us keep the SDKs and systems they interact with secure. diff --git a/api.md b/api.md new file mode 100644 index 00000000..ec9b481c --- /dev/null +++ b/api.md @@ -0,0 +1,25 @@ +# Apps + +Types: + +```python +from kernel.types import AppDeployResponse, AppInvokeResponse, AppRetrieveInvocationResponse +``` + +Methods: + +- client.apps.deploy(\*\*params) -> AppDeployResponse +- client.apps.invoke(\*\*params) -> AppInvokeResponse +- client.apps.retrieve_invocation(id) -> AppRetrieveInvocationResponse + +# Browser + +Types: + +```python +from kernel.types import BrowserCreateSessionResponse +``` + +Methods: + +- client.browser.create_session() -> BrowserCreateSessionResponse diff --git a/bin/publish-pypi b/bin/publish-pypi new file mode 100644 index 00000000..826054e9 --- /dev/null +++ b/bin/publish-pypi @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -eux +mkdir -p dist +rye build --clean +rye publish --yes --token=$PYPI_TOKEN diff --git a/examples/.keep b/examples/.keep new file mode 100644 index 00000000..d8c73e93 --- /dev/null +++ b/examples/.keep @@ -0,0 +1,4 @@ +File generated from our OpenAPI spec by Stainless. + +This directory can be used to store example files demonstrating usage of this SDK. +It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..0745431c --- /dev/null +++ b/mypy.ini @@ -0,0 +1,50 @@ +[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/kernel/_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 new file mode 100644 index 00000000..53bca7ff --- /dev/null +++ b/noxfile.py @@ -0,0 +1,9 @@ +import nox + + +@nox.session(reuse_venv=True, name="test-pydantic-v1") +def test_pydantic_v1(session: nox.Session) -> None: + session.install("-r", "requirements-dev.lock") + session.install("pydantic<2") + + session.run("pytest", "--showlocals", "--ignore=tests/functional", *session.posargs) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..b3465a4f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,207 @@ +[project] +name = "kernel" +version = "0.0.1-alpha.0" +description = "The official Python library for the kernel API" +dynamic = ["readme"] +license = "Apache-2.0" +authors = [ +{ name = "Kernel", email = "" }, +] +dependencies = [ + "httpx>=0.23.0, <1", + "pydantic>=1.9.0, <3", + "typing-extensions>=4.10, <5", + "anyio>=3.5.0, <5", + "distro>=1.7.0, <2", + "sniffio", +] +requires-python = ">= 3.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", + "Operating System :: OS Independent", + "Operating System :: POSIX", + "Operating System :: MacOS", + "Operating System :: POSIX :: Linux", + "Operating System :: Microsoft :: Windows", + "Topic :: Software Development :: Libraries :: Python Modules", + "License :: OSI Approved :: Apache Software License" +] + +[project.urls] +Homepage = "https://github.com/stainless-sdks/kernel-python" +Repository = "https://github.com/stainless-sdks/kernel-python" + + +[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", +] + +[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 kernel'" + +typecheck = { chain = [ + "typecheck:pyright", + "typecheck:mypy" +]} +"typecheck:pyright" = "pyright" +"typecheck:verify-types" = "pyright --verifytypes kernel --ignoreexternal" +"typecheck:mypy" = "mypy ." + +[build-system] +requires = ["hatchling==1.26.3", "hatch-fancy-pypi-readme"] +build-backend = "hatchling.build" + +[tool.hatch.build] +include = [ + "src/*" +] + +[tool.hatch.build.targets.wheel] +packages = ["src/kernel"] + +[tool.hatch.build.targets.sdist] +# Basically everything except hidden files/directories (such as .github, .devcontainers, .python-version, etc) +include = [ + "/*.toml", + "/*.json", + "/*.lock", + "/*.md", + "/mypy.ini", + "/noxfile.py", + "bin/*", + "examples/*", + "src/*", + "tests/*", +] + +[tool.hatch.metadata.hooks.fancy-pypi-readme] +content-type = "text/markdown" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] +path = "README.md" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]] +# replace relative links with absolute links +pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)' +replacement = '[\1](https://github.com/stainless-sdks/kernel-python/tree/main/\g<2>)' + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = "--tb=short" +xfail_strict = true +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "session" +filterwarnings = [ + "error" +] + +[tool.pyright] +# this enables practically every flag given by pyright. +# there are a couple of flags that are still disabled by +# default in strict mode as they are experimental and niche. +typeCheckingMode = "strict" +pythonVersion = "3.8" + +exclude = [ + "_dev", + ".venv", + ".nox", +] + +reportImplicitOverride = true +reportOverlappingOverload = false + +reportImportCycles = false +reportPrivateUsage = false + +[tool.ruff] +line-length = 120 +output-format = "grouped" +target-version = "py37" + +[tool.ruff.format] +docstring-code-format = true + +[tool.ruff.lint] +select = [ + # isort + "I", + # bugbear rules + "B", + # remove unused imports + "F401", + # bare except statements + "E722", + # unused arguments + "ARG", + # print statements + "T201", + "T203", + # misuse of typing.TYPE_CHECKING + "TC004", + # import rules + "TID251", +] +ignore = [ + # mutable defaults + "B006", +] +unfixable = [ + # disable auto fix for print statements + "T201", + "T203", +] + +[tool.ruff.lint.flake8-tidy-imports.banned-api] +"functools.lru_cache".msg = "This function does not retain type information for the wrapped function's arguments; The `lru_cache` function from `_utils` should be used instead" + +[tool.ruff.lint.isort] +length-sort = true +length-sort-straight = true +combine-as-imports = true +extra-standard-library = ["typing_extensions"] +known-first-party = ["kernel", "tests"] + +[tool.ruff.lint.per-file-ignores] +"bin/**.py" = ["T201", "T203"] +"scripts/**.py" = ["T201", "T203"] +"tests/**.py" = ["T201", "T203"] +"examples/**.py" = ["T201", "T203"] diff --git a/requirements-dev.lock b/requirements-dev.lock new file mode 100644 index 00000000..efd90ead --- /dev/null +++ b/requirements-dev.lock @@ -0,0 +1,104 @@ +# 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:. +annotated-types==0.6.0 + # via pydantic +anyio==4.4.0 + # via httpx + # via kernel +argcomplete==3.1.2 + # via nox +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 kernel +exceptiongroup==1.2.2 + # via anyio + # via pytest +filelock==3.12.4 + # via virtualenv +h11==0.14.0 + # via httpcore +httpcore==1.0.2 + # via httpx +httpx==0.28.1 + # via kernel + # via respx +idna==3.4 + # via anyio + # via httpx +importlib-metadata==7.0.0 +iniconfig==2.0.0 + # via pytest +markdown-it-py==3.0.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py +mypy==1.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 +pydantic==2.10.3 + # via kernel +pydantic-core==2.27.1 + # via pydantic +pygments==2.18.0 + # via rich +pyright==1.1.399 +pytest==8.3.3 + # via pytest-asyncio +pytest-asyncio==0.24.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 kernel +time-machine==2.9.0 +tomli==2.0.2 + # via mypy + # via pytest +typing-extensions==4.12.2 + # via anyio + # via kernel + # via mypy + # via pydantic + # via pydantic-core + # via pyright +virtualenv==20.24.5 + # via nox +zipp==3.17.0 + # via importlib-metadata diff --git a/requirements.lock b/requirements.lock new file mode 100644 index 00000000..40719199 --- /dev/null +++ b/requirements.lock @@ -0,0 +1,45 @@ +# generated by rye +# use `rye lock` or `rye sync` to update this lockfile +# +# last locked with the following flags: +# pre: false +# features: [] +# all-features: true +# with-sources: false +# generate-hashes: false +# universal: false + +-e file:. +annotated-types==0.6.0 + # via pydantic +anyio==4.4.0 + # via httpx + # via kernel +certifi==2023.7.22 + # via httpcore + # via httpx +distro==1.8.0 + # via kernel +exceptiongroup==1.2.2 + # via anyio +h11==0.14.0 + # via httpcore +httpcore==1.0.2 + # via httpx +httpx==0.28.1 + # via kernel +idna==3.4 + # via anyio + # via httpx +pydantic==2.10.3 + # via kernel +pydantic-core==2.27.1 + # via pydantic +sniffio==1.3.0 + # via anyio + # via kernel +typing-extensions==4.12.2 + # via anyio + # via kernel + # via pydantic + # via pydantic-core diff --git a/scripts/bootstrap b/scripts/bootstrap new file mode 100755 index 00000000..e84fe62c --- /dev/null +++ b/scripts/bootstrap @@ -0,0 +1,19 @@ +#!/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 new file mode 100755 index 00000000..667ec2d7 --- /dev/null +++ b/scripts/format @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +echo "==> Running formatters" +rye run format diff --git a/scripts/lint b/scripts/lint new file mode 100755 index 00000000..b5b88913 --- /dev/null +++ b/scripts/lint @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +echo "==> Running lints" +rye run lint + +echo "==> Making sure it imports" +rye run python -c 'import kernel' diff --git a/scripts/mock b/scripts/mock new file mode 100755 index 00000000..d2814ae6 --- /dev/null +++ b/scripts/mock @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +if [[ -n "$1" && "$1" != '--'* ]]; then + URL="$1" + shift +else + URL="$(grep 'openapi_spec_url' .stats.yml | cut -d' ' -f2)" +fi + +# Check if the URL is empty +if [ -z "$URL" ]; then + echo "Error: No OpenAPI spec path/url provided or found in .stats.yml" + exit 1 +fi + +echo "==> Starting mock server with URL ${URL}" + +# Run prism mock on the given spec +if [ "$1" == "--daemon" ]; then + npm exec --package=@stainless-api/prism-cli@5.8.5 -- prism mock "$URL" &> .prism.log & + + # Wait for server to come online + echo -n "Waiting for server" + while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do + echo -n "." + sleep 0.1 + done + + if grep -q "✖ fatal" ".prism.log"; then + cat .prism.log + exit 1 + fi + + echo +else + npm exec --package=@stainless-api/prism-cli@5.8.5 -- prism mock "$URL" +fi diff --git a/scripts/test b/scripts/test new file mode 100755 index 00000000..2b878456 --- /dev/null +++ b/scripts/test @@ -0,0 +1,61 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' # No Color + +function prism_is_running() { + curl --silent "http://localhost:4010" >/dev/null 2>&1 +} + +kill_server_on_port() { + pids=$(lsof -t -i tcp:"$1" || echo "") + if [ "$pids" != "" ]; then + kill "$pids" + echo "Stopped $pids." + fi +} + +function is_overriding_api_base_url() { + [ -n "$TEST_API_BASE_URL" ] +} + +if ! is_overriding_api_base_url && ! prism_is_running ; then + # When we exit this script, make sure to kill the background mock server process + trap 'kill_server_on_port 4010' EXIT + + # Start the dev server + ./scripts/mock --daemon +fi + +if is_overriding_api_base_url ; then + echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}" + echo +elif ! prism_is_running ; then + echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server" + echo -e "running against your OpenAPI spec." + echo + echo -e "To run the server, pass in the path or url of your OpenAPI" + echo -e "spec to the prism command:" + echo + echo -e " \$ ${YELLOW}npm exec --package=@stoplight/prism-cli@~5.3.2 -- prism mock path/to/your.openapi.yml${NC}" + echo + + exit 1 +else + echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}" + echo +fi + +export DEFER_PYDANTIC_BUILD=false + +echo "==> Running tests" +rye run pytest "$@" + +echo "==> Running Pydantic v1 tests" +rye run nox -s test-pydantic-v1 -- "$@" diff --git a/scripts/utils/ruffen-docs.py b/scripts/utils/ruffen-docs.py new file mode 100644 index 00000000..0cf2bd2f --- /dev/null +++ b/scripts/utils/ruffen-docs.py @@ -0,0 +1,167 @@ +# fork of https://github.com/asottile/blacken-docs adapted for ruff +from __future__ import annotations + +import re +import sys +import argparse +import textwrap +import contextlib +import subprocess +from typing import Match, Optional, Sequence, Generator, NamedTuple, cast + +MD_RE = re.compile( + r"(?P^(?P *)```\s*python\n)" r"(?P.*?)" r"(?P^(?P=indent)```\s*$)", + re.DOTALL | re.MULTILINE, +) +MD_PYCON_RE = re.compile( + r"(?P^(?P *)```\s*pycon\n)" r"(?P.*?)" r"(?P^(?P=indent)```.*$)", + re.DOTALL | re.MULTILINE, +) +PYCON_PREFIX = ">>> " +PYCON_CONTINUATION_PREFIX = "..." +PYCON_CONTINUATION_RE = re.compile( + rf"^{re.escape(PYCON_CONTINUATION_PREFIX)}( |$)", +) +DEFAULT_LINE_LENGTH = 100 + + +class CodeBlockError(NamedTuple): + offset: int + exc: Exception + + +def format_str( + src: str, +) -> tuple[str, Sequence[CodeBlockError]]: + errors: list[CodeBlockError] = [] + + @contextlib.contextmanager + def _collect_error(match: Match[str]) -> Generator[None, None, None]: + try: + yield + except Exception as e: + errors.append(CodeBlockError(match.start(), e)) + + def _md_match(match: Match[str]) -> str: + code = textwrap.dedent(match["code"]) + with _collect_error(match): + code = format_code_block(code) + code = textwrap.indent(code, match["indent"]) + return f"{match['before']}{code}{match['after']}" + + def _pycon_match(match: Match[str]) -> str: + code = "" + fragment = cast(Optional[str], None) + + def finish_fragment() -> None: + nonlocal code + nonlocal fragment + + if fragment is not None: + with _collect_error(match): + fragment = format_code_block(fragment) + fragment_lines = fragment.splitlines() + code += f"{PYCON_PREFIX}{fragment_lines[0]}\n" + for line in fragment_lines[1:]: + # Skip blank lines to handle Black adding a blank above + # functions within blocks. A blank line would end the REPL + # continuation prompt. + # + # >>> if True: + # ... def f(): + # ... pass + # ... + if line: + code += f"{PYCON_CONTINUATION_PREFIX} {line}\n" + if fragment_lines[-1].startswith(" "): + code += f"{PYCON_CONTINUATION_PREFIX}\n" + fragment = None + + indentation = None + for line in match["code"].splitlines(): + orig_line, line = line, line.lstrip() + if indentation is None and line: + indentation = len(orig_line) - len(line) + continuation_match = PYCON_CONTINUATION_RE.match(line) + if continuation_match and fragment is not None: + fragment += line[continuation_match.end() :] + "\n" + else: + finish_fragment() + if line.startswith(PYCON_PREFIX): + fragment = line[len(PYCON_PREFIX) :] + "\n" + else: + code += orig_line[indentation:] + "\n" + finish_fragment() + return code + + def _md_pycon_match(match: Match[str]) -> str: + code = _pycon_match(match) + code = textwrap.indent(code, match["indent"]) + return f"{match['before']}{code}{match['after']}" + + src = MD_RE.sub(_md_match, src) + src = MD_PYCON_RE.sub(_md_pycon_match, src) + return src, errors + + +def format_code_block(code: str) -> str: + return subprocess.check_output( + [ + sys.executable, + "-m", + "ruff", + "format", + "--stdin-filename=script.py", + f"--line-length={DEFAULT_LINE_LENGTH}", + ], + encoding="utf-8", + input=code, + ) + + +def format_file( + filename: str, + skip_errors: bool, +) -> int: + with open(filename, encoding="UTF-8") as f: + contents = f.read() + new_contents, errors = format_str(contents) + for error in errors: + lineno = contents[: error.offset].count("\n") + 1 + print(f"{filename}:{lineno}: code block parse error {error.exc}") + if errors and not skip_errors: + return 1 + if contents != new_contents: + print(f"{filename}: Rewriting...") + with open(filename, "w", encoding="UTF-8") as f: + f.write(new_contents) + return 0 + else: + return 0 + + +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser() + parser.add_argument( + "-l", + "--line-length", + type=int, + default=DEFAULT_LINE_LENGTH, + ) + parser.add_argument( + "-S", + "--skip-string-normalization", + action="store_true", + ) + parser.add_argument("-E", "--skip-errors", action="store_true") + parser.add_argument("filenames", nargs="*") + args = parser.parse_args(argv) + + retv = 0 + for filename in args.filenames: + retv |= format_file(filename, skip_errors=args.skip_errors) + return retv + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/kernel/__init__.py b/src/kernel/__init__.py new file mode 100644 index 00000000..1093761b --- /dev/null +++ b/src/kernel/__init__.py @@ -0,0 +1,84 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from . import types +from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes +from ._utils import file_from_path +from ._client import Client, Kernel, Stream, Timeout, Transport, AsyncClient, AsyncKernel, AsyncStream, RequestOptions +from ._models import BaseModel +from ._version import __title__, __version__ +from ._response import APIResponse as APIResponse, AsyncAPIResponse as AsyncAPIResponse +from ._constants import DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES, DEFAULT_CONNECTION_LIMITS +from ._exceptions import ( + APIError, + KernelError, + ConflictError, + NotFoundError, + APIStatusError, + RateLimitError, + APITimeoutError, + BadRequestError, + APIConnectionError, + AuthenticationError, + InternalServerError, + PermissionDeniedError, + UnprocessableEntityError, + APIResponseValidationError, +) +from ._base_client import DefaultHttpxClient, DefaultAsyncHttpxClient +from ._utils._logs import setup_logging as _setup_logging + +__all__ = [ + "types", + "__version__", + "__title__", + "NoneType", + "Transport", + "ProxiesTypes", + "NotGiven", + "NOT_GIVEN", + "Omit", + "KernelError", + "APIError", + "APIStatusError", + "APITimeoutError", + "APIConnectionError", + "APIResponseValidationError", + "BadRequestError", + "AuthenticationError", + "PermissionDeniedError", + "NotFoundError", + "ConflictError", + "UnprocessableEntityError", + "RateLimitError", + "InternalServerError", + "Timeout", + "RequestOptions", + "Client", + "AsyncClient", + "Stream", + "AsyncStream", + "Kernel", + "AsyncKernel", + "file_from_path", + "BaseModel", + "DEFAULT_TIMEOUT", + "DEFAULT_MAX_RETRIES", + "DEFAULT_CONNECTION_LIMITS", + "DefaultHttpxClient", + "DefaultAsyncHttpxClient", +] + +_setup_logging() + +# Update the __module__ attribute for exported symbols so that +# error messages point to this module instead of the module +# it was originally defined in, e.g. +# kernel._exceptions.NotFoundError -> kernel.NotFoundError +__locals = locals() +for __name in __all__: + if not __name.startswith("__"): + try: + __locals[__name].__module__ = "kernel" + except (TypeError, AttributeError): + # Some of our exported symbols are builtins which we can't set attributes for. + pass diff --git a/src/kernel/_base_client.py b/src/kernel/_base_client.py new file mode 100644 index 00000000..34308dde --- /dev/null +++ b/src/kernel/_base_client.py @@ -0,0 +1,1943 @@ +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 `kernel.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("_", "-")} + + # TODO: report this error to httpx + return self._client.build_request( # pyright: ignore[reportUnknownMemberType] + headers=headers, + timeout=self.timeout if isinstance(options.timeout, NotGiven) else options.timeout, + method=options.method, + url=prepared_url, + # the `Query` type that we use is incompatible with qs' + # `Params` type as it needs to be typed as `Mapping[str, object]` + # so that passing a `TypedDict` doesn't cause an error. + # https://github.com/microsoft/pyright/issues/3526#event-6715453066 + params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None, + json=json_data if is_given(json_data) else None, + files=files, + **kwargs, + ) + + def _serialize_multipartform(self, data: Mapping[object, object]) -> dict[str, object]: + items = self.qs.stringify_items( + # TODO: type ignore is required as stringify_items is well typed but we can't be + # well typed without heavy validation. + data, # type: ignore + array_format="brackets", + ) + serialized: dict[str, object] = {} + for key, value in items: + existing = serialized.get(key) + + if not existing: + serialized[key] = value + continue + + # If a value has already been set for this key then that + # means we're sending data like `array[]=[1, 2, 3]` and we + # need to tell httpx that we want to send multiple values with + # the same key which is done by using a list or a tuple. + # + # Note: 2d arrays should never result in the same key at both + # levels so it's safe to assume that if the value is a list, + # it was because we changed it to be a list. + if is_list(existing): + existing.append(value) + else: + serialized[key] = [existing, value] + + return serialized + + def _maybe_override_cast_to(self, cast_to: type[ResponseT], options: FinalRequestOptions) -> type[ResponseT]: + if not is_given(options.headers): + return cast_to + + # make a copy of the headers so we don't mutate user-input + headers = dict(options.headers) + + # we internally support defining a temporary header to override the + # default `cast_to` type for use with `.with_raw_response` and `.with_streaming_response` + # see _response.py for implementation details + override_cast_to = headers.pop(OVERRIDE_CAST_TO_HEADER, NOT_GIVEN) + if is_given(override_cast_to): + options.headers = headers + return cast(Type[ResponseT], override_cast_to) + + return cast_to + + def _should_stream_response_body(self, request: httpx.Request) -> bool: + return request.headers.get(RAW_RESPONSE_HEADER) == "stream" # type: ignore[no-any-return] + + def _process_response_data( + self, + *, + data: object, + cast_to: type[ResponseT], + response: httpx.Response, + ) -> ResponseT: + if data is None: + return cast(ResponseT, None) + + if cast_to is object: + return cast(ResponseT, data) + + try: + if inspect.isclass(cast_to) and issubclass(cast_to, ModelBuilderProtocol): + return cast(ResponseT, cast_to.build(response=response, data=data)) + + if self._strict_response_validation: + return cast(ResponseT, validate_type(type_=cast_to, value=data)) + + return cast(ResponseT, construct_type(type_=cast_to, value=data)) + except pydantic.ValidationError as err: + raise APIResponseValidationError(response=response, body=data) from err + + @property + def qs(self) -> Querystring: + return Querystring() + + @property + def custom_auth(self) -> httpx.Auth | None: + return None + + @property + def auth_headers(self) -> dict[str, str]: + return {} + + @property + def default_headers(self) -> dict[str, str | Omit]: + return { + "Accept": "application/json", + "Content-Type": "application/json", + "User-Agent": self.user_agent, + **self.platform_headers(), + **self.auth_headers, + **self._custom_headers, + } + + @property + def default_query(self) -> dict[str, object]: + return { + **self._custom_query, + } + + def _validate_headers( + self, + headers: Headers, # noqa: ARG002 + custom_headers: Headers, # noqa: ARG002 + ) -> None: + """Validate the given default headers and custom headers. + + Does nothing by default. + """ + return + + @property + def user_agent(self) -> str: + return f"{self.__class__.__name__}/Python {self._version}" + + @property + def base_url(self) -> URL: + return self._base_url + + @base_url.setter + def base_url(self, url: URL | str) -> None: + self._base_url = self._enforce_trailing_slash(url if isinstance(url, URL) else URL(url)) + + def platform_headers(self) -> Dict[str, str]: + # the actual implementation is in a separate `lru_cache` decorated + # function because adding `lru_cache` to methods will leak memory + # https://github.com/python/cpython/issues/88476 + return platform_headers(self._version, platform=self._platform) + + def _parse_retry_after_header(self, response_headers: Optional[httpx.Headers] = None) -> float | None: + """Returns a float of the number of seconds (not milliseconds) to wait after retrying, or None if unspecified. + + About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After + See also https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After#syntax + """ + if response_headers is None: + return None + + # First, try the non-standard `retry-after-ms` header for milliseconds, + # which is more precise than integer-seconds `retry-after` + try: + retry_ms_header = response_headers.get("retry-after-ms", None) + return float(retry_ms_header) / 1000 + except (TypeError, ValueError): + pass + + # Next, try parsing `retry-after` header as seconds (allowing nonstandard floats). + retry_header = response_headers.get("retry-after") + try: + # note: the spec indicates that this should only ever be an integer + # but if someone sends a float there's no reason for us to not respect it + return float(retry_header) + except (TypeError, ValueError): + pass + + # Last, try parsing `retry-after` as a date. + retry_date_tuple = email.utils.parsedate_tz(retry_header) + if retry_date_tuple is None: + return None + + retry_date = email.utils.mktime_tz(retry_date_tuple) + return float(retry_date - time.time()) + + def _calculate_retry_timeout( + self, + remaining_retries: int, + options: FinalRequestOptions, + response_headers: Optional[httpx.Headers] = None, + ) -> float: + max_retries = options.get_max_retries(self.max_retries) + + # If the API asks us to wait a certain amount of time (and it's a reasonable amount), just do what it says. + retry_after = self._parse_retry_after_header(response_headers) + if retry_after is not None and 0 < retry_after <= 60: + return retry_after + + # Also cap retry count to 1000 to avoid any potential overflows with `pow` + nb_retries = min(max_retries - remaining_retries, 1000) + + # Apply exponential backoff, but not more than the max. + sleep_seconds = min(INITIAL_RETRY_DELAY * pow(2.0, nb_retries), MAX_RETRY_DELAY) + + # Apply some jitter, plus-or-minus half a second. + jitter = 1 - 0.25 * random() + timeout = sleep_seconds * jitter + return timeout if timeout >= 0 else 0 + + def _should_retry(self, response: httpx.Response) -> bool: + # Note: this is not a standard header + should_retry_header = response.headers.get("x-should-retry") + + # If the server explicitly says whether or not to retry, obey. + if should_retry_header == "true": + log.debug("Retrying as header `x-should-retry` is set to `true`") + return True + if should_retry_header == "false": + log.debug("Not retrying as header `x-should-retry` is set to `false`") + return False + + # Retry on request timeouts. + if response.status_code == 408: + log.debug("Retrying due to status code %i", response.status_code) + return True + + # Retry on lock timeouts. + if response.status_code == 409: + log.debug("Retrying due to status code %i", response.status_code) + return True + + # Retry on rate limits. + if response.status_code == 429: + log.debug("Retrying due to status code %i", response.status_code) + return True + + # Retry internal errors. + if response.status_code >= 500: + log.debug("Retrying due to status code %i", response.status_code) + return True + + log.debug("Not retrying") + return False + + def _idempotency_key(self) -> str: + return f"stainless-python-retry-{uuid.uuid4()}" + + +class _DefaultHttpxClient(httpx.Client): + def __init__(self, **kwargs: Any) -> None: + kwargs.setdefault("timeout", DEFAULT_TIMEOUT) + kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) + kwargs.setdefault("follow_redirects", True) + super().__init__(**kwargs) + + +if TYPE_CHECKING: + DefaultHttpxClient = httpx.Client + """An alias to `httpx.Client` that provides the same defaults that this SDK + uses internally. + + This is useful because overriding the `http_client` with your own instance of + `httpx.Client` will result in httpx's defaults being used, not ours. + """ +else: + DefaultHttpxClient = _DefaultHttpxClient + + +class SyncHttpxClientWrapper(DefaultHttpxClient): + def __del__(self) -> None: + 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 + + 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): + if not issubclass(origin, APIResponse): + raise TypeError(f"API Response types must subclass {APIResponse}; Received {origin}") + + response_cls = cast("type[BaseAPIResponse[Any]]", cast_to) + return cast( + ResponseT, + response_cls( + raw=response, + client=self, + cast_to=extract_response_type(response_cls), + stream=stream, + stream_cls=stream_cls, + options=options, + retries_taken=retries_taken, + ), + ) + + if cast_to == httpx.Response: + return cast(ResponseT, response) + + api_response = APIResponse( + raw=response, + client=self, + cast_to=cast("type[ResponseT]", cast_to), # pyright: ignore[reportUnnecessaryCast] + stream=stream, + stream_cls=stream_cls, + options=options, + retries_taken=retries_taken, + ) + if bool(response.request.headers.get(RAW_RESPONSE_HEADER)): + return cast(ResponseT, api_response) + + return api_response.parse() + + def _request_api_list( + self, + model: Type[object], + page: Type[SyncPageT], + options: FinalRequestOptions, + ) -> SyncPageT: + def _parser(resp: SyncPageT) -> SyncPageT: + resp._set_private_attributes( + client=self, + model=model, + options=options, + ) + return resp + + options.post_parser = _parser + + return self.request(page, options, stream=False) + + @overload + def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: Literal[False] = False, + ) -> ResponseT: ... + + @overload + def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: Literal[True], + stream_cls: type[_StreamT], + ) -> _StreamT: ... + + @overload + def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: bool, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: ... + + def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: bool = False, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: + opts = FinalRequestOptions.construct(method="get", url=path, **options) + # cast is required because mypy complains about returning Any even though + # it understands the type variables + return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) + + @overload + def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + files: RequestFiles | None = None, + stream: Literal[False] = False, + ) -> ResponseT: ... + + @overload + def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + files: RequestFiles | None = None, + stream: Literal[True], + stream_cls: type[_StreamT], + ) -> _StreamT: ... + + @overload + def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + files: RequestFiles | None = None, + stream: bool, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: ... + + def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + files: RequestFiles | None = None, + stream: bool = False, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: + opts = FinalRequestOptions.construct( + method="post", url=path, json_data=body, files=to_httpx_files(files), **options + ) + return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) + + def patch( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + return self.request(cast_to, opts) + + def put( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct( + method="put", url=path, json_data=body, files=to_httpx_files(files), **options + ) + return self.request(cast_to, opts) + + def delete( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + return self.request(cast_to, opts) + + def get_api_list( + self, + path: str, + *, + model: Type[object], + page: Type[SyncPageT], + body: Body | None = None, + options: RequestOptions = {}, + method: str = "get", + ) -> SyncPageT: + opts = FinalRequestOptions.construct(method=method, url=path, json_data=body, **options) + return self._request_api_list(model, page, opts) + + +class _DefaultAsyncHttpxClient(httpx.AsyncClient): + def __init__(self, **kwargs: Any) -> None: + kwargs.setdefault("timeout", DEFAULT_TIMEOUT) + kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) + kwargs.setdefault("follow_redirects", True) + super().__init__(**kwargs) + + +if TYPE_CHECKING: + DefaultAsyncHttpxClient = httpx.AsyncClient + """An alias to `httpx.AsyncClient` that provides the same defaults that this SDK + uses internally. + + This is useful because overriding the `http_client` with your own instance of + `httpx.AsyncClient` will result in httpx's defaults being used, not ours. + """ +else: + DefaultAsyncHttpxClient = _DefaultAsyncHttpxClient + + +class AsyncHttpxClientWrapper(DefaultAsyncHttpxClient): + def __del__(self) -> None: + 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 + + 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): + 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/kernel/_client.py b/src/kernel/_client.py new file mode 100644 index 00000000..aa9f2279 --- /dev/null +++ b/src/kernel/_client.py @@ -0,0 +1,402 @@ +# 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 apps, browser +from ._streaming import Stream as Stream, AsyncStream as AsyncStream +from ._exceptions import KernelError, APIStatusError +from ._base_client import ( + DEFAULT_MAX_RETRIES, + SyncAPIClient, + AsyncAPIClient, +) + +__all__ = ["Timeout", "Transport", "ProxiesTypes", "RequestOptions", "Kernel", "AsyncKernel", "Client", "AsyncClient"] + + +class Kernel(SyncAPIClient): + apps: apps.AppsResource + browser: browser.BrowserResource + with_raw_response: KernelWithRawResponse + with_streaming_response: KernelWithStreamedResponse + + # 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 Kernel client instance. + + This automatically infers the `api_key` argument from the `KERNEL_API_KEY` environment variable if it is not provided. + """ + if api_key is None: + api_key = os.environ.get("KERNEL_API_KEY") + if api_key is None: + raise KernelError( + "The api_key client option must be set either by passing api_key to the client or by setting the KERNEL_API_KEY environment variable" + ) + self.api_key = api_key + + if base_url is None: + base_url = os.environ.get("KERNEL_BASE_URL") + if base_url is None: + base_url = f"http://localhost:3001" + + 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.apps = apps.AppsResource(self) + self.browser = browser.BrowserResource(self) + self.with_raw_response = KernelWithRawResponse(self) + self.with_streaming_response = KernelWithStreamedResponse(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 {"Authorization": f"Bearer {api_key}"} + + @property + @override + def default_headers(self) -> dict[str, str | Omit]: + return { + **super().default_headers, + "X-Stainless-Async": "false", + **self._custom_headers, + } + + 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 AsyncKernel(AsyncAPIClient): + apps: apps.AsyncAppsResource + browser: browser.AsyncBrowserResource + with_raw_response: AsyncKernelWithRawResponse + with_streaming_response: AsyncKernelWithStreamedResponse + + # 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 AsyncKernel client instance. + + This automatically infers the `api_key` argument from the `KERNEL_API_KEY` environment variable if it is not provided. + """ + if api_key is None: + api_key = os.environ.get("KERNEL_API_KEY") + if api_key is None: + raise KernelError( + "The api_key client option must be set either by passing api_key to the client or by setting the KERNEL_API_KEY environment variable" + ) + self.api_key = api_key + + if base_url is None: + base_url = os.environ.get("KERNEL_BASE_URL") + if base_url is None: + base_url = f"http://localhost:3001" + + 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.apps = apps.AsyncAppsResource(self) + self.browser = browser.AsyncBrowserResource(self) + self.with_raw_response = AsyncKernelWithRawResponse(self) + self.with_streaming_response = AsyncKernelWithStreamedResponse(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 {"Authorization": f"Bearer {api_key}"} + + @property + @override + def default_headers(self) -> dict[str, str | Omit]: + return { + **super().default_headers, + "X-Stainless-Async": f"async:{get_async_library()}", + **self._custom_headers, + } + + 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 KernelWithRawResponse: + def __init__(self, client: Kernel) -> None: + self.apps = apps.AppsResourceWithRawResponse(client.apps) + self.browser = browser.BrowserResourceWithRawResponse(client.browser) + + +class AsyncKernelWithRawResponse: + def __init__(self, client: AsyncKernel) -> None: + self.apps = apps.AsyncAppsResourceWithRawResponse(client.apps) + self.browser = browser.AsyncBrowserResourceWithRawResponse(client.browser) + + +class KernelWithStreamedResponse: + def __init__(self, client: Kernel) -> None: + self.apps = apps.AppsResourceWithStreamingResponse(client.apps) + self.browser = browser.BrowserResourceWithStreamingResponse(client.browser) + + +class AsyncKernelWithStreamedResponse: + def __init__(self, client: AsyncKernel) -> None: + self.apps = apps.AsyncAppsResourceWithStreamingResponse(client.apps) + self.browser = browser.AsyncBrowserResourceWithStreamingResponse(client.browser) + + +Client = Kernel + +AsyncClient = AsyncKernel diff --git a/src/kernel/_compat.py b/src/kernel/_compat.py new file mode 100644 index 00000000..92d9ee61 --- /dev/null +++ b/src/kernel/_compat.py @@ -0,0 +1,219 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload +from datetime import date, datetime +from typing_extensions import Self, Literal + +import pydantic +from pydantic.fields import FieldInfo + +from ._types import IncEx, StrBytesIntFloat + +_T = TypeVar("_T") +_ModelT = TypeVar("_ModelT", bound=pydantic.BaseModel) + +# --------------- Pydantic v2 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/kernel/_constants.py b/src/kernel/_constants.py new file mode 100644 index 00000000..6ddf2c71 --- /dev/null +++ b/src/kernel/_constants.py @@ -0,0 +1,14 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import httpx + +RAW_RESPONSE_HEADER = "X-Stainless-Raw-Response" +OVERRIDE_CAST_TO_HEADER = "____stainless_override_cast_to" + +# default timeout is 1 minute +DEFAULT_TIMEOUT = httpx.Timeout(timeout=60, connect=5.0) +DEFAULT_MAX_RETRIES = 2 +DEFAULT_CONNECTION_LIMITS = httpx.Limits(max_connections=100, max_keepalive_connections=20) + +INITIAL_RETRY_DELAY = 0.5 +MAX_RETRY_DELAY = 8.0 diff --git a/src/kernel/_exceptions.py b/src/kernel/_exceptions.py new file mode 100644 index 00000000..53cd14ce --- /dev/null +++ b/src/kernel/_exceptions.py @@ -0,0 +1,108 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal + +import httpx + +__all__ = [ + "BadRequestError", + "AuthenticationError", + "PermissionDeniedError", + "NotFoundError", + "ConflictError", + "UnprocessableEntityError", + "RateLimitError", + "InternalServerError", +] + + +class KernelError(Exception): + pass + + +class APIError(KernelError): + 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/kernel/_files.py b/src/kernel/_files.py new file mode 100644 index 00000000..df2a05e5 --- /dev/null +++ b/src/kernel/_files.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +import io +import os +import pathlib +from typing import overload +from typing_extensions import TypeGuard + +import anyio + +from ._types import ( + FileTypes, + FileContent, + RequestFiles, + HttpxFileTypes, + Base64FileInput, + HttpxFileContent, + HttpxRequestFiles, +) +from ._utils import is_tuple_t, is_mapping_t, is_sequence_t + + +def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]: + return isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike) + + +def is_file_content(obj: object) -> TypeGuard[FileContent]: + return ( + isinstance(obj, bytes) or isinstance(obj, tuple) or isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike) + ) + + +def assert_is_file_content(obj: object, *, key: str | None = None) -> None: + if not is_file_content(obj): + prefix = f"Expected entry at `{key}`" if key is not None else f"Expected file input `{obj!r}`" + raise RuntimeError( + f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead. See https://github.com/stainless-sdks/kernel-python/tree/main#file-uploads" + ) 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/kernel/_models.py b/src/kernel/_models.py new file mode 100644 index 00000000..798956f1 --- /dev/null +++ b/src/kernel/_models.py @@ -0,0 +1,803 @@ +from __future__ import annotations + +import os +import inspect +from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, cast +from datetime import date, datetime +from typing_extensions import ( + Unpack, + Literal, + ClassVar, + Protocol, + Required, + ParamSpec, + TypedDict, + TypeGuard, + final, + override, + runtime_checkable, +) + +import pydantic +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 = {} + for key, value in values.items(): + if key not in model_fields: + if PYDANTIC_V2: + _extra[key] = value + else: + _fields_set.add(key) + fields_values[key] = value + + object.__setattr__(m, "__dict__", fields_values) + + if PYDANTIC_V2: + # these properties are copied from Pydantic's `model_construct()` method + object.__setattr__(m, "__pydantic_private__", None) + object.__setattr__(m, "__pydantic_extra__", _extra) + object.__setattr__(m, "__pydantic_fields_set__", _fields_set) + else: + # init_private_attributes() does not exist in v2 + m._init_private_attributes() # type: ignore + + # copied from Pydantic v1's `construct()` method + object.__setattr__(m, "__fields_set__", _fields_set) + + return m + + if not TYPE_CHECKING: + # type checkers incorrectly complain about this assignment + # because the type signatures are technically different + # although not in practice + model_construct = construct + + if not PYDANTIC_V2: + # we define aliases for some of the new pydantic v2 methods so + # that we can just document these methods without having to specify + # a specific pydantic version as some users may not know which + # pydantic version they are currently using + + @override + def model_dump( + self, + *, + mode: Literal["json", "python"] | str = "python", + include: IncEx | None = 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_) + + +def is_basemodel(type_: type) -> bool: + """Returns whether or not the given type is either a `BaseModel` or a union of `BaseModel`""" + if is_union(type_): + for variant in get_args(type_): + if is_basemodel(variant): + return True + + return False + + return is_basemodel_type(type_) + + +def is_basemodel_type(type_: type) -> TypeGuard[type[BaseModel] | type[GenericModel]]: + origin = get_origin(type_) or type_ + if not inspect.isclass(origin): + return False + return issubclass(origin, BaseModel) or issubclass(origin, GenericModel) + + +def build( + base_model_cls: Callable[P, _BaseModelT], + *args: P.args, + **kwargs: P.kwargs, +) -> _BaseModelT: + """Construct a BaseModel class without validation. + + This is useful for cases where you need to instantiate a `BaseModel` + from an API response as this provides type-safe params which isn't supported + by helpers like `construct_type()`. + + ```py + build(MyModel, my_field_a="foo", my_field_b=123) + ``` + """ + if args: + raise TypeError( + "Received positional arguments which are not supported; Keyword arguments must be used instead", + ) + + return cast(_BaseModelT, construct_type(type_=base_model_cls, value=kwargs)) + + +def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T: + """Loose coercion to the expected type with construction of nested values. + + Note: the returned value from this function is not guaranteed to match the + given type. + """ + return cast(_T, construct_type(value=value, type_=type_)) + + +def construct_type(*, value: object, type_: object) -> object: + """Loose coercion to the expected type with construction of nested values. + + If the given value does not match the expected type then it is returned as-is. + """ + + # 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 is_annotated_type(type_): + meta: tuple[Any, ...] = get_args(type_)[1:] + type_ = extract_type_arg(type_, 0) + else: + meta = tuple() + + # we need to use the origin class for any types that are subscripted generics + # e.g. Dict[str, object] + origin = get_origin(type_) or type_ + args = get_args(type_) + + if is_union(origin): + try: + return validate_type(type_=cast("type[object]", 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 + + +@final +class FinalRequestOptions(pydantic.BaseModel): + method: str + url: str + params: Query = {} + headers: Union[Headers, NotGiven] = NotGiven() + max_retries: Union[int, NotGiven] = NotGiven() + timeout: Union[float, Timeout, None, NotGiven] = NotGiven() + files: Union[HttpxRequestFiles, None] = None + idempotency_key: Union[str, None] = None + post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() + + # It should be noted that we cannot use `json` here as that would override + # a BaseModel method in an incompatible fashion. + json_data: Union[Body, None] = None + extra_json: Union[AnyMapping, None] = None + + if PYDANTIC_V2: + model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True) + else: + + class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated] + arbitrary_types_allowed: bool = True + + def get_max_retries(self, max_retries: int) -> int: + if isinstance(self.max_retries, NotGiven): + return max_retries + return self.max_retries + + def _strip_raw_response_header(self) -> None: + if not is_given(self.headers): + return + + if self.headers.get(RAW_RESPONSE_HEADER): + self.headers = {**self.headers} + self.headers.pop(RAW_RESPONSE_HEADER) + + # override the `construct` method so that we can run custom transformations. + # this is necessary as we don't want to do any actual runtime type checking + # (which means we can't use validators) but we do want to ensure that `NotGiven` + # values are not present + # + # type ignore required because we're adding explicit types to `**values` + @classmethod + def construct( # type: ignore + cls, + _fields_set: set[str] | None = None, + **values: Unpack[FinalRequestOptionsInput], + ) -> FinalRequestOptions: + kwargs: dict[str, Any] = { + # we unconditionally call `strip_not_given` on any value + # as it will just ignore any non-mapping types + key: strip_not_given(value) + for key, value in values.items() + } + if PYDANTIC_V2: + return super().model_construct(_fields_set, **kwargs) + return cast(FinalRequestOptions, super().construct(_fields_set, **kwargs)) # pyright: ignore[reportDeprecated] + + if not TYPE_CHECKING: + # type checkers incorrectly complain about this assignment + model_construct = construct diff --git a/src/kernel/_qs.py b/src/kernel/_qs.py new file mode 100644 index 00000000..274320ca --- /dev/null +++ b/src/kernel/_qs.py @@ -0,0 +1,150 @@ +from __future__ import annotations + +from typing import Any, List, Tuple, Union, Mapping, TypeVar +from urllib.parse import parse_qs, urlencode +from typing_extensions import Literal, get_args + +from ._types import NOT_GIVEN, NotGiven, NotGivenOr +from ._utils import flatten + +_T = TypeVar("_T") + + +ArrayFormat = Literal["comma", "repeat", "indices", "brackets"] +NestedFormat = Literal["dots", "brackets"] + +PrimitiveData = Union[str, int, float, bool, None] +# this should be Data = Union[PrimitiveData, "List[Data]", "Tuple[Data]", "Mapping[str, Data]"] +# https://github.com/microsoft/pyright/issues/3555 +Data = Union[PrimitiveData, List[Any], Tuple[Any], "Mapping[str, Any]"] +Params = Mapping[str, Data] + + +class Querystring: + array_format: ArrayFormat + nested_format: NestedFormat + + def __init__( + self, + *, + array_format: ArrayFormat = "repeat", + nested_format: NestedFormat = "brackets", + ) -> None: + self.array_format = array_format + self.nested_format = nested_format + + def parse(self, query: str) -> Mapping[str, object]: + # Note: custom format syntax is not supported yet + return parse_qs(query) + + def stringify( + self, + params: Params, + *, + array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, + nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, + ) -> str: + return urlencode( + self.stringify_items( + params, + array_format=array_format, + nested_format=nested_format, + ) + ) + + def stringify_items( + self, + params: Params, + *, + array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, + nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, + ) -> list[tuple[str, str]]: + opts = Options( + qs=self, + array_format=array_format, + nested_format=nested_format, + ) + return flatten([self._stringify_item(key, value, opts) for key, value in params.items()]) + + def _stringify_item( + self, + key: str, + value: Data, + opts: Options, + ) -> list[tuple[str, str]]: + if isinstance(value, Mapping): + items: list[tuple[str, str]] = [] + nested_format = opts.nested_format + for subkey, subvalue in value.items(): + items.extend( + self._stringify_item( + # TODO: error if unknown format + f"{key}.{subkey}" if nested_format == "dots" else f"{key}[{subkey}]", + subvalue, + opts, + ) + ) + return items + + if isinstance(value, (list, tuple)): + array_format = opts.array_format + if array_format == "comma": + return [ + ( + key, + ",".join(self._primitive_value_to_str(item) for item in value if item is not None), + ), + ] + elif array_format == "repeat": + items = [] + for item in value: + items.extend(self._stringify_item(key, item, opts)) + return items + elif array_format == "indices": + raise NotImplementedError("The array indices format is not supported yet") + elif array_format == "brackets": + items = [] + key = key + "[]" + for item in value: + items.extend(self._stringify_item(key, item, opts)) + return items + else: + raise NotImplementedError( + f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}" + ) + + serialised = self._primitive_value_to_str(value) + if not serialised: + return [] + return [(key, serialised)] + + def _primitive_value_to_str(self, value: PrimitiveData) -> str: + # copied from httpx + if value is True: + return "true" + elif value is False: + return "false" + elif value is None: + return "" + return str(value) + + +_qs = Querystring() +parse = _qs.parse +stringify = _qs.stringify +stringify_items = _qs.stringify_items + + +class Options: + array_format: ArrayFormat + nested_format: NestedFormat + + def __init__( + self, + qs: Querystring = _qs, + *, + array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, + nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, + ) -> None: + self.array_format = qs.array_format if isinstance(array_format, NotGiven) else array_format + self.nested_format = qs.nested_format if isinstance(nested_format, NotGiven) else nested_format diff --git a/src/kernel/_resource.py b/src/kernel/_resource.py new file mode 100644 index 00000000..eb51ab58 --- /dev/null +++ b/src/kernel/_resource.py @@ -0,0 +1,43 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import time +from typing import TYPE_CHECKING + +import anyio + +if TYPE_CHECKING: + from ._client import Kernel, AsyncKernel + + +class SyncAPIResource: + _client: Kernel + + def __init__(self, client: Kernel) -> 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: AsyncKernel + + def __init__(self, client: AsyncKernel) -> 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/kernel/_response.py b/src/kernel/_response.py new file mode 100644 index 00000000..89c72c39 --- /dev/null +++ b/src/kernel/_response.py @@ -0,0 +1,830 @@ +from __future__ import annotations + +import os +import inspect +import logging +import datetime +import functools +from types import TracebackType +from typing import ( + TYPE_CHECKING, + Any, + Union, + Generic, + TypeVar, + Callable, + Iterator, + AsyncIterator, + cast, + overload, +) +from typing_extensions import Awaitable, ParamSpec, override, get_origin + +import anyio +import httpx +import pydantic + +from ._types import NoneType +from ._utils import is_given, extract_type_arg, is_annotated_type, is_type_alias_type, extract_type_var_from_base +from ._models import BaseModel, is_basemodel +from ._constants import RAW_RESPONSE_HEADER, OVERRIDE_CAST_TO_HEADER +from ._streaming import Stream, AsyncStream, is_stream_class_type, extract_stream_chunk_type +from ._exceptions import KernelError, 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 kernel 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 kernel 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 kernel 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 `kernel._streaming` for reference", + ) + + +class StreamAlreadyConsumed(KernelError): + """ + 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/kernel/_streaming.py b/src/kernel/_streaming.py new file mode 100644 index 00000000..e3131a3b --- /dev/null +++ b/src/kernel/_streaming.py @@ -0,0 +1,333 @@ +# Note: initially copied from https://github.com/florimondmanca/httpx-sse/blob/master/src/httpx_sse/_decoders.py +from __future__ import annotations + +import json +import inspect +from types import TracebackType +from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, AsyncIterator, cast +from typing_extensions import Self, Protocol, TypeGuard, override, get_origin, runtime_checkable + +import httpx + +from ._utils import extract_type_var_from_base + +if TYPE_CHECKING: + from ._client import Kernel, AsyncKernel + + +_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: Kernel, + ) -> 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: AsyncKernel, + ) -> 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/kernel/_types.py b/src/kernel/_types.py new file mode 100644 index 00000000..2b0c5c3c --- /dev/null +++ b/src/kernel/_types.py @@ -0,0 +1,217 @@ +from __future__ import annotations + +from os import PathLike +from typing import ( + IO, + TYPE_CHECKING, + Any, + Dict, + List, + Type, + Tuple, + Union, + Mapping, + TypeVar, + Callable, + Optional, + Sequence, +) +from typing_extensions import 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 kernel import NoneType +# client.get('/foo', cast_to=NoneType) +# +# or build it yourself: +# +# client.get('/foo', cast_to=type(None)) +if TYPE_CHECKING: + NoneType: Type[None] +else: + NoneType = type(None) + + +class RequestOptions(TypedDict, total=False): + headers: Headers + max_retries: int + timeout: float | Timeout | None + params: Query + extra_json: AnyMapping + idempotency_key: str + + +# Sentinel class used until PEP 0661 is accepted +class NotGiven: + """ + A sentinel singleton class used to distinguish omitted keyword arguments + from those passed in with the value None (which may have different behavior). + + For example: + + ```py + def get(timeout: Union[int, NotGiven, None] = NotGiven()) -> Response: ... + + + get(timeout=1) # 1s timeout + get(timeout=None) # No timeout + get() # Default timeout behavior, which may not be statically known at the method definition. + ``` + """ + + def __bool__(self) -> Literal[False]: + return False + + @override + def __repr__(self) -> str: + return "NOT_GIVEN" + + +NotGivenOr = Union[_T, NotGiven] +NOT_GIVEN = NotGiven() + + +class Omit: + """In certain situations you need to be able to represent a case where a default value has + to be explicitly removed and `None` is not an appropriate substitute, for example: + + ```py + # as the default `Content-Type` header is `application/json` that will be sent + client.post("/upload/files", files={"file": b"my raw file content"}) + + # you can't explicitly override the header as it has to be dynamically generated + # to look something like: 'multipart/form-data; boundary=0d8382fcf5f8c3be01ca2e11002d2983' + client.post(..., headers={"Content-Type": "multipart/form-data"}) + + # instead you can remove the default `application/json` header by passing Omit + client.post(..., headers={"Content-Type": Omit()}) + ``` + """ + + def __bool__(self) -> Literal[False]: + return False + + +@runtime_checkable +class ModelBuilderProtocol(Protocol): + @classmethod + def build( + cls: type[_T], + *, + response: Response, + data: object, + ) -> _T: ... + + +Headers = Mapping[str, Union[str, Omit]] + + +class HeadersLikeProtocol(Protocol): + def get(self, __key: str) -> str | None: ... + + +HeadersLike = Union[Headers, HeadersLikeProtocol] + +ResponseT = TypeVar( + "ResponseT", + bound=Union[ + object, + str, + None, + "BaseModel", + List[Any], + Dict[str, Any], + Response, + ModelBuilderProtocol, + "APIResponse[Any]", + "AsyncAPIResponse[Any]", + ], +) + +StrBytesIntFloat = Union[str, bytes, int, float] + +# Note: copied from Pydantic +# https://github.com/pydantic/pydantic/blob/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 diff --git a/src/kernel/_utils/__init__.py b/src/kernel/_utils/__init__.py new file mode 100644 index 00000000..d4fda26f --- /dev/null +++ b/src/kernel/_utils/__init__.py @@ -0,0 +1,57 @@ +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/kernel/_utils/_logs.py b/src/kernel/_utils/_logs.py new file mode 100644 index 00000000..4eff94ba --- /dev/null +++ b/src/kernel/_utils/_logs.py @@ -0,0 +1,25 @@ +import os +import logging + +logger: logging.Logger = logging.getLogger("kernel") +httpx_logger: logging.Logger = logging.getLogger("httpx") + + +def _basic_config() -> None: + # e.g. [2023-10-05 14:12:26 - kernel._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("KERNEL_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/kernel/_utils/_proxy.py b/src/kernel/_utils/_proxy.py new file mode 100644 index 00000000..0f239a33 --- /dev/null +++ b/src/kernel/_utils/_proxy.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Generic, TypeVar, Iterable, cast +from typing_extensions import override + +T = TypeVar("T") + + +class LazyProxy(Generic[T], ABC): + """Implements data methods to pretend that an instance is another instance. + + This includes forwarding attribute access and other methods. + """ + + # Note: we have to special case proxies that themselves return proxies + # to support using a proxy as a catch-all for any random access, e.g. `proxy.foo.bar.baz` + + def __getattr__(self, attr: str) -> object: + proxied = self.__get_proxied__() + if isinstance(proxied, LazyProxy): + return proxied # pyright: ignore + return getattr(proxied, attr) + + @override + def __repr__(self) -> str: + proxied = self.__get_proxied__() + if isinstance(proxied, LazyProxy): + return proxied.__class__.__name__ + return repr(self.__get_proxied__()) + + @override + def __str__(self) -> str: + proxied = self.__get_proxied__() + if isinstance(proxied, LazyProxy): + return proxied.__class__.__name__ + return str(proxied) + + @override + def __dir__(self) -> Iterable[str]: + proxied = self.__get_proxied__() + if isinstance(proxied, LazyProxy): + return [] + return proxied.__dir__() + + @property # type: ignore + @override + def __class__(self) -> type: # pyright: ignore + try: + proxied = self.__get_proxied__() + except Exception: + return type(self) + if issubclass(type(proxied), LazyProxy): + return type(proxied) + return proxied.__class__ + + def __get_proxied__(self) -> T: + return self.__load__() + + def __as_proxied__(self) -> T: + """Helper method that returns the current proxy, typed as the loaded object""" + return cast(T, self) + + @abstractmethod + def __load__(self) -> T: ... diff --git a/src/kernel/_utils/_reflection.py b/src/kernel/_utils/_reflection.py new file mode 100644 index 00000000..89aa712a --- /dev/null +++ b/src/kernel/_utils/_reflection.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import inspect +from typing import Any, Callable + + +def function_has_argument(func: Callable[..., Any], arg_name: str) -> bool: + """Returns whether or not the given function has a specific parameter""" + sig = inspect.signature(func) + return arg_name in sig.parameters + + +def assert_signatures_in_sync( + source_func: Callable[..., Any], + check_func: Callable[..., Any], + *, + exclude_params: set[str] = set(), +) -> None: + """Ensure that the signature of the second function matches the first.""" + + check_sig = inspect.signature(check_func) + source_sig = inspect.signature(source_func) + + errors: list[str] = [] + + for name, source_param in source_sig.parameters.items(): + if name in exclude_params: + continue + + custom_param = check_sig.parameters.get(name) + if not custom_param: + errors.append(f"the `{name}` param is missing") + continue + + if custom_param.annotation != source_param.annotation: + errors.append( + f"types for the `{name}` param are do not match; source={repr(source_param.annotation)} checking={repr(custom_param.annotation)}" + ) + continue + + if errors: + raise AssertionError(f"{len(errors)} errors encountered when comparing signatures:\n\n" + "\n\n".join(errors)) diff --git a/src/kernel/_utils/_streams.py b/src/kernel/_utils/_streams.py new file mode 100644 index 00000000..f4a0208f --- /dev/null +++ b/src/kernel/_utils/_streams.py @@ -0,0 +1,12 @@ +from typing import Any +from typing_extensions import Iterator, AsyncIterator + + +def consume_sync_iterator(iterator: Iterator[Any]) -> None: + for _ in iterator: + ... + + +async def consume_async_iterator(iterator: AsyncIterator[Any]) -> None: + async for _ in iterator: + ... diff --git a/src/kernel/_utils/_sync.py b/src/kernel/_utils/_sync.py new file mode 100644 index 00000000..ad7ec71b --- /dev/null +++ b/src/kernel/_utils/_sync.py @@ -0,0 +1,86 @@ +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/kernel/_utils/_transform.py b/src/kernel/_utils/_transform.py new file mode 100644 index 00000000..b0cc20a7 --- /dev/null +++ b/src/kernel/_utils/_transform.py @@ -0,0 +1,447 @@ +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/kernel/_utils/_typing.py b/src/kernel/_utils/_typing.py new file mode 100644 index 00000000..1bac9542 --- /dev/null +++ b/src/kernel/_utils/_typing.py @@ -0,0 +1,151 @@ +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/kernel/_utils/_utils.py b/src/kernel/_utils/_utils.py new file mode 100644 index 00000000..ea3cf3f2 --- /dev/null +++ b/src/kernel/_utils/_utils.py @@ -0,0 +1,422 @@ +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/kernel/_version.py b/src/kernel/_version.py new file mode 100644 index 00000000..3d085d7e --- /dev/null +++ b/src/kernel/_version.py @@ -0,0 +1,4 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +__title__ = "kernel" +__version__ = "0.0.1-alpha.0" diff --git a/src/kernel/lib/.keep b/src/kernel/lib/.keep new file mode 100644 index 00000000..5e2c99fd --- /dev/null +++ b/src/kernel/lib/.keep @@ -0,0 +1,4 @@ +File generated from our OpenAPI spec by Stainless. + +This directory can be used to store custom files to expand the SDK. +It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file diff --git a/src/kernel/py.typed b/src/kernel/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/src/kernel/resources/__init__.py b/src/kernel/resources/__init__.py new file mode 100644 index 00000000..a0d1ea6f --- /dev/null +++ b/src/kernel/resources/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .apps import ( + AppsResource, + AsyncAppsResource, + AppsResourceWithRawResponse, + AsyncAppsResourceWithRawResponse, + AppsResourceWithStreamingResponse, + AsyncAppsResourceWithStreamingResponse, +) +from .browser import ( + BrowserResource, + AsyncBrowserResource, + BrowserResourceWithRawResponse, + AsyncBrowserResourceWithRawResponse, + BrowserResourceWithStreamingResponse, + AsyncBrowserResourceWithStreamingResponse, +) + +__all__ = [ + "AppsResource", + "AsyncAppsResource", + "AppsResourceWithRawResponse", + "AsyncAppsResourceWithRawResponse", + "AppsResourceWithStreamingResponse", + "AsyncAppsResourceWithStreamingResponse", + "BrowserResource", + "AsyncBrowserResource", + "BrowserResourceWithRawResponse", + "AsyncBrowserResourceWithRawResponse", + "BrowserResourceWithStreamingResponse", + "AsyncBrowserResourceWithStreamingResponse", +] diff --git a/src/kernel/resources/apps.py b/src/kernel/resources/apps.py new file mode 100644 index 00000000..74c900c6 --- /dev/null +++ b/src/kernel/resources/apps.py @@ -0,0 +1,401 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Mapping, cast + +import httpx + +from ..types import app_deploy_params, app_invoke_params +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes +from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.app_deploy_response import AppDeployResponse +from ..types.app_invoke_response import AppInvokeResponse +from ..types.app_retrieve_invocation_response import AppRetrieveInvocationResponse + +__all__ = ["AppsResource", "AsyncAppsResource"] + + +class AppsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> AppsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/kernel-python#accessing-raw-response-data-eg-headers + """ + return AppsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AppsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/kernel-python#with_streaming_response + """ + return AppsResourceWithStreamingResponse(self) + + def deploy( + self, + *, + app_name: str, + file: FileTypes, + version: str, + region: 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, + ) -> AppDeployResponse: + """ + Deploy a new application + + Args: + app_name: Name of the application + + file: ZIP file containing the application + + version: Version of the application + + region: AWS region for deployment (e.g. "aws.us-east-1a") + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "app_name": app_name, + "file": file, + "version": version, + "region": region, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return self._post( + "/apps/deploy", + body=maybe_transform(body, app_deploy_params.AppDeployParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AppDeployResponse, + ) + + def invoke( + self, + *, + app_name: str, + payload: object, + version: 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, + ) -> AppInvokeResponse: + """ + Invoke an application + + Args: + app_name: Name of the application + + payload: Input data for the application + + version: Version of the application + + 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( + "/apps/invoke", + body=maybe_transform( + { + "app_name": app_name, + "payload": payload, + "version": version, + }, + app_invoke_params.AppInvokeParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AppInvokeResponse, + ) + + def retrieve_invocation( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AppRetrieveInvocationResponse: + """ + Get an app invocation by id + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/apps/invocations/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AppRetrieveInvocationResponse, + ) + + +class AsyncAppsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncAppsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/kernel-python#accessing-raw-response-data-eg-headers + """ + return AsyncAppsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAppsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/kernel-python#with_streaming_response + """ + return AsyncAppsResourceWithStreamingResponse(self) + + async def deploy( + self, + *, + app_name: str, + file: FileTypes, + version: str, + region: 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, + ) -> AppDeployResponse: + """ + Deploy a new application + + Args: + app_name: Name of the application + + file: ZIP file containing the application + + version: Version of the application + + region: AWS region for deployment (e.g. "aws.us-east-1a") + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "app_name": app_name, + "file": file, + "version": version, + "region": region, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return await self._post( + "/apps/deploy", + body=await async_maybe_transform(body, app_deploy_params.AppDeployParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AppDeployResponse, + ) + + async def invoke( + self, + *, + app_name: str, + payload: object, + version: 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, + ) -> AppInvokeResponse: + """ + Invoke an application + + Args: + app_name: Name of the application + + payload: Input data for the application + + version: Version of the application + + 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( + "/apps/invoke", + body=await async_maybe_transform( + { + "app_name": app_name, + "payload": payload, + "version": version, + }, + app_invoke_params.AppInvokeParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AppInvokeResponse, + ) + + async def retrieve_invocation( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AppRetrieveInvocationResponse: + """ + Get an app invocation by id + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/apps/invocations/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AppRetrieveInvocationResponse, + ) + + +class AppsResourceWithRawResponse: + def __init__(self, apps: AppsResource) -> None: + self._apps = apps + + self.deploy = to_raw_response_wrapper( + apps.deploy, + ) + self.invoke = to_raw_response_wrapper( + apps.invoke, + ) + self.retrieve_invocation = to_raw_response_wrapper( + apps.retrieve_invocation, + ) + + +class AsyncAppsResourceWithRawResponse: + def __init__(self, apps: AsyncAppsResource) -> None: + self._apps = apps + + self.deploy = async_to_raw_response_wrapper( + apps.deploy, + ) + self.invoke = async_to_raw_response_wrapper( + apps.invoke, + ) + self.retrieve_invocation = async_to_raw_response_wrapper( + apps.retrieve_invocation, + ) + + +class AppsResourceWithStreamingResponse: + def __init__(self, apps: AppsResource) -> None: + self._apps = apps + + self.deploy = to_streamed_response_wrapper( + apps.deploy, + ) + self.invoke = to_streamed_response_wrapper( + apps.invoke, + ) + self.retrieve_invocation = to_streamed_response_wrapper( + apps.retrieve_invocation, + ) + + +class AsyncAppsResourceWithStreamingResponse: + def __init__(self, apps: AsyncAppsResource) -> None: + self._apps = apps + + self.deploy = async_to_streamed_response_wrapper( + apps.deploy, + ) + self.invoke = async_to_streamed_response_wrapper( + apps.invoke, + ) + self.retrieve_invocation = async_to_streamed_response_wrapper( + apps.retrieve_invocation, + ) diff --git a/src/kernel/resources/browser.py b/src/kernel/resources/browser.py new file mode 100644 index 00000000..ae79d382 --- /dev/null +++ b/src/kernel/resources/browser.py @@ -0,0 +1,135 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.browser_create_session_response import BrowserCreateSessionResponse + +__all__ = ["BrowserResource", "AsyncBrowserResource"] + + +class BrowserResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> BrowserResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/kernel-python#accessing-raw-response-data-eg-headers + """ + return BrowserResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> BrowserResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/kernel-python#with_streaming_response + """ + return BrowserResourceWithStreamingResponse(self) + + def create_session( + 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, + ) -> BrowserCreateSessionResponse: + """Create Browser Session""" + return self._post( + "/browser", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BrowserCreateSessionResponse, + ) + + +class AsyncBrowserResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncBrowserResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/kernel-python#accessing-raw-response-data-eg-headers + """ + return AsyncBrowserResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncBrowserResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/kernel-python#with_streaming_response + """ + return AsyncBrowserResourceWithStreamingResponse(self) + + async def create_session( + 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, + ) -> BrowserCreateSessionResponse: + """Create Browser Session""" + return await self._post( + "/browser", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BrowserCreateSessionResponse, + ) + + +class BrowserResourceWithRawResponse: + def __init__(self, browser: BrowserResource) -> None: + self._browser = browser + + self.create_session = to_raw_response_wrapper( + browser.create_session, + ) + + +class AsyncBrowserResourceWithRawResponse: + def __init__(self, browser: AsyncBrowserResource) -> None: + self._browser = browser + + self.create_session = async_to_raw_response_wrapper( + browser.create_session, + ) + + +class BrowserResourceWithStreamingResponse: + def __init__(self, browser: BrowserResource) -> None: + self._browser = browser + + self.create_session = to_streamed_response_wrapper( + browser.create_session, + ) + + +class AsyncBrowserResourceWithStreamingResponse: + def __init__(self, browser: AsyncBrowserResource) -> None: + self._browser = browser + + self.create_session = async_to_streamed_response_wrapper( + browser.create_session, + ) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py new file mode 100644 index 00000000..9577c2f8 --- /dev/null +++ b/src/kernel/types/__init__.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .app_deploy_params import AppDeployParams as AppDeployParams +from .app_invoke_params import AppInvokeParams as AppInvokeParams +from .app_deploy_response import AppDeployResponse as AppDeployResponse +from .app_invoke_response import AppInvokeResponse as AppInvokeResponse +from .browser_create_session_response import BrowserCreateSessionResponse as BrowserCreateSessionResponse +from .app_retrieve_invocation_response import AppRetrieveInvocationResponse as AppRetrieveInvocationResponse diff --git a/src/kernel/types/app_deploy_params.py b/src/kernel/types/app_deploy_params.py new file mode 100644 index 00000000..78a11f26 --- /dev/null +++ b/src/kernel/types/app_deploy_params.py @@ -0,0 +1,24 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, Annotated, TypedDict + +from .._types import FileTypes +from .._utils import PropertyInfo + +__all__ = ["AppDeployParams"] + + +class AppDeployParams(TypedDict, total=False): + app_name: Required[Annotated[str, PropertyInfo(alias="appName")]] + """Name of the application""" + + file: Required[FileTypes] + """ZIP file containing the application""" + + version: Required[str] + """Version of the application""" + + region: str + """AWS region for deployment (e.g. "aws.us-east-1a")""" diff --git a/src/kernel/types/app_deploy_response.py b/src/kernel/types/app_deploy_response.py new file mode 100644 index 00000000..6a214df6 --- /dev/null +++ b/src/kernel/types/app_deploy_response.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["AppDeployResponse"] + + +class AppDeployResponse(BaseModel): + id: str + """ID of the deployed app version""" + + message: str + """Success message""" + + success: bool + """Status of the deployment""" diff --git a/src/kernel/types/app_invoke_params.py b/src/kernel/types/app_invoke_params.py new file mode 100644 index 00000000..773fd459 --- /dev/null +++ b/src/kernel/types/app_invoke_params.py @@ -0,0 +1,20 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, Annotated, TypedDict + +from .._utils import PropertyInfo + +__all__ = ["AppInvokeParams"] + + +class AppInvokeParams(TypedDict, total=False): + app_name: Required[Annotated[str, PropertyInfo(alias="appName")]] + """Name of the application""" + + payload: Required[object] + """Input data for the application""" + + version: Required[str] + """Version of the application""" diff --git a/src/kernel/types/app_invoke_response.py b/src/kernel/types/app_invoke_response.py new file mode 100644 index 00000000..801ec5cc --- /dev/null +++ b/src/kernel/types/app_invoke_response.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["AppInvokeResponse"] + + +class AppInvokeResponse(BaseModel): + id: str + """ID of the invocation""" + + status: str + """Status of the invocation""" diff --git a/src/kernel/types/app_retrieve_invocation_response.py b/src/kernel/types/app_retrieve_invocation_response.py new file mode 100644 index 00000000..8b3de1f8 --- /dev/null +++ b/src/kernel/types/app_retrieve_invocation_response.py @@ -0,0 +1,25 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["AppRetrieveInvocationResponse"] + + +class AppRetrieveInvocationResponse(BaseModel): + id: str + + app_name: str = FieldInfo(alias="appName") + + finished_at: Optional[str] = FieldInfo(alias="finishedAt", default=None) + + input: str + + output: str + + started_at: str = FieldInfo(alias="startedAt") + + status: str diff --git a/src/kernel/types/browser_create_session_response.py b/src/kernel/types/browser_create_session_response.py new file mode 100644 index 00000000..d4e46da8 --- /dev/null +++ b/src/kernel/types/browser_create_session_response.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["BrowserCreateSessionResponse"] + + +class BrowserCreateSessionResponse(BaseModel): + cdp_ws_url: str + """Websocket URL for Chrome DevTools Protocol connections to the browser session""" + + remote_url: str + """Remote URL for live viewing the browser session""" + + session_id: str = FieldInfo(alias="sessionId") + """Unique identifier for the browser session""" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..fd8019a9 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/__init__.py b/tests/api_resources/__init__.py new file mode 100644 index 00000000..fd8019a9 --- /dev/null +++ b/tests/api_resources/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/test_apps.py b/tests/api_resources/test_apps.py new file mode 100644 index 00000000..7f086b59 --- /dev/null +++ b/tests/api_resources/test_apps.py @@ -0,0 +1,292 @@ +# 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 kernel import Kernel, AsyncKernel +from tests.utils import assert_matches_type +from kernel.types import ( + AppDeployResponse, + AppInvokeResponse, + AppRetrieveInvocationResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestApps: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip() + @parametrize + def test_method_deploy(self, client: Kernel) -> None: + app = client.apps.deploy( + app_name="my-awesome-app", + file=b"raw file contents", + version="1.0.0", + ) + assert_matches_type(AppDeployResponse, app, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_method_deploy_with_all_params(self, client: Kernel) -> None: + app = client.apps.deploy( + app_name="my-awesome-app", + file=b"raw file contents", + version="1.0.0", + region="aws.us-east-1a", + ) + assert_matches_type(AppDeployResponse, app, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_raw_response_deploy(self, client: Kernel) -> None: + response = client.apps.with_raw_response.deploy( + app_name="my-awesome-app", + file=b"raw file contents", + version="1.0.0", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + app = response.parse() + assert_matches_type(AppDeployResponse, app, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_streaming_response_deploy(self, client: Kernel) -> None: + with client.apps.with_streaming_response.deploy( + app_name="my-awesome-app", + file=b"raw file contents", + version="1.0.0", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + app = response.parse() + assert_matches_type(AppDeployResponse, app, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + def test_method_invoke(self, client: Kernel) -> None: + app = client.apps.invoke( + app_name="my-awesome-app", + payload='{ "data": "example input" }', + version="1.0.0", + ) + assert_matches_type(AppInvokeResponse, app, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_raw_response_invoke(self, client: Kernel) -> None: + response = client.apps.with_raw_response.invoke( + app_name="my-awesome-app", + payload='{ "data": "example input" }', + version="1.0.0", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + app = response.parse() + assert_matches_type(AppInvokeResponse, app, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_streaming_response_invoke(self, client: Kernel) -> None: + with client.apps.with_streaming_response.invoke( + app_name="my-awesome-app", + payload='{ "data": "example input" }', + version="1.0.0", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + app = response.parse() + assert_matches_type(AppInvokeResponse, app, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + def test_method_retrieve_invocation(self, client: Kernel) -> None: + app = client.apps.retrieve_invocation( + "id", + ) + assert_matches_type(AppRetrieveInvocationResponse, app, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_raw_response_retrieve_invocation(self, client: Kernel) -> None: + response = client.apps.with_raw_response.retrieve_invocation( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + app = response.parse() + assert_matches_type(AppRetrieveInvocationResponse, app, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_streaming_response_retrieve_invocation(self, client: Kernel) -> None: + with client.apps.with_streaming_response.retrieve_invocation( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + app = response.parse() + assert_matches_type(AppRetrieveInvocationResponse, app, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + def test_path_params_retrieve_invocation(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.apps.with_raw_response.retrieve_invocation( + "", + ) + + +class TestAsyncApps: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip() + @parametrize + async def test_method_deploy(self, async_client: AsyncKernel) -> None: + app = await async_client.apps.deploy( + app_name="my-awesome-app", + file=b"raw file contents", + version="1.0.0", + ) + assert_matches_type(AppDeployResponse, app, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_method_deploy_with_all_params(self, async_client: AsyncKernel) -> None: + app = await async_client.apps.deploy( + app_name="my-awesome-app", + file=b"raw file contents", + version="1.0.0", + region="aws.us-east-1a", + ) + assert_matches_type(AppDeployResponse, app, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_raw_response_deploy(self, async_client: AsyncKernel) -> None: + response = await async_client.apps.with_raw_response.deploy( + app_name="my-awesome-app", + file=b"raw file contents", + version="1.0.0", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + app = await response.parse() + assert_matches_type(AppDeployResponse, app, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_deploy(self, async_client: AsyncKernel) -> None: + async with async_client.apps.with_streaming_response.deploy( + app_name="my-awesome-app", + file=b"raw file contents", + version="1.0.0", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + app = await response.parse() + assert_matches_type(AppDeployResponse, app, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + async def test_method_invoke(self, async_client: AsyncKernel) -> None: + app = await async_client.apps.invoke( + app_name="my-awesome-app", + payload='{ "data": "example input" }', + version="1.0.0", + ) + assert_matches_type(AppInvokeResponse, app, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_raw_response_invoke(self, async_client: AsyncKernel) -> None: + response = await async_client.apps.with_raw_response.invoke( + app_name="my-awesome-app", + payload='{ "data": "example input" }', + version="1.0.0", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + app = await response.parse() + assert_matches_type(AppInvokeResponse, app, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_invoke(self, async_client: AsyncKernel) -> None: + async with async_client.apps.with_streaming_response.invoke( + app_name="my-awesome-app", + payload='{ "data": "example input" }', + version="1.0.0", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + app = await response.parse() + assert_matches_type(AppInvokeResponse, app, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + async def test_method_retrieve_invocation(self, async_client: AsyncKernel) -> None: + app = await async_client.apps.retrieve_invocation( + "id", + ) + assert_matches_type(AppRetrieveInvocationResponse, app, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_raw_response_retrieve_invocation(self, async_client: AsyncKernel) -> None: + response = await async_client.apps.with_raw_response.retrieve_invocation( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + app = await response.parse() + assert_matches_type(AppRetrieveInvocationResponse, app, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_retrieve_invocation(self, async_client: AsyncKernel) -> None: + async with async_client.apps.with_streaming_response.retrieve_invocation( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + app = await response.parse() + assert_matches_type(AppRetrieveInvocationResponse, app, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + async def test_path_params_retrieve_invocation(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.apps.with_raw_response.retrieve_invocation( + "", + ) diff --git a/tests/api_resources/test_browser.py b/tests/api_resources/test_browser.py new file mode 100644 index 00000000..1aa4a1c8 --- /dev/null +++ b/tests/api_resources/test_browser.py @@ -0,0 +1,78 @@ +# 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 kernel import Kernel, AsyncKernel +from tests.utils import assert_matches_type +from kernel.types import BrowserCreateSessionResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestBrowser: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip() + @parametrize + def test_method_create_session(self, client: Kernel) -> None: + browser = client.browser.create_session() + assert_matches_type(BrowserCreateSessionResponse, browser, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_raw_response_create_session(self, client: Kernel) -> None: + response = client.browser.with_raw_response.create_session() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser = response.parse() + assert_matches_type(BrowserCreateSessionResponse, browser, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_streaming_response_create_session(self, client: Kernel) -> None: + with client.browser.with_streaming_response.create_session() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser = response.parse() + assert_matches_type(BrowserCreateSessionResponse, browser, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncBrowser: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip() + @parametrize + async def test_method_create_session(self, async_client: AsyncKernel) -> None: + browser = await async_client.browser.create_session() + assert_matches_type(BrowserCreateSessionResponse, browser, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_raw_response_create_session(self, async_client: AsyncKernel) -> None: + response = await async_client.browser.with_raw_response.create_session() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser = await response.parse() + assert_matches_type(BrowserCreateSessionResponse, browser, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_create_session(self, async_client: AsyncKernel) -> None: + async with async_client.browser.with_streaming_response.create_session() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser = await response.parse() + assert_matches_type(BrowserCreateSessionResponse, browser, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..6d3cc202 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +import os +import logging +from typing import TYPE_CHECKING, Iterator, AsyncIterator + +import pytest +from pytest_asyncio import is_async_test + +from kernel import Kernel, AsyncKernel + +if TYPE_CHECKING: + from _pytest.fixtures import FixtureRequest # pyright: ignore[reportPrivateImportUsage] + +pytest.register_assert_rewrite("tests.utils") + +logging.getLogger("kernel").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) + + +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[Kernel]: + strict = getattr(request, "param", True) + if not isinstance(strict, bool): + raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") + + with Kernel(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[AsyncKernel]: + strict = getattr(request, "param", True) + if not isinstance(strict, bool): + raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") + + async with AsyncKernel(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client: + yield client diff --git a/tests/sample_file.txt b/tests/sample_file.txt new file mode 100644 index 00000000..af5626b4 --- /dev/null +++ b/tests/sample_file.txt @@ -0,0 +1 @@ +Hello, world! diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 00000000..f989403e --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,1680 @@ +# 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 kernel import Kernel, AsyncKernel, APIResponseValidationError +from kernel._types import Omit +from kernel._utils import maybe_transform +from kernel._models import BaseModel, FinalRequestOptions +from kernel._constants import RAW_RESPONSE_HEADER +from kernel._exceptions import KernelError, APIStatusError, APITimeoutError, APIResponseValidationError +from kernel._base_client import DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, BaseClient, make_request_options +from kernel.types.app_deploy_params import AppDeployParams + +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: Kernel | AsyncKernel) -> int: + transport = client._client._transport + assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) + + pool = transport._pool + return len(pool._requests) + + +class TestKernel: + client = Kernel(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 = Kernel( + 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 = Kernel( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} + ) + assert _get_params(client)["foo"] == "bar" + + # does not override the already given value when not specified + copied = client.copy() + assert _get_params(copied)["foo"] == "bar" + + # merges already given params + copied = client.copy(default_query={"bar": "stainless"}) + params = _get_params(copied) + assert params["foo"] == "bar" + assert params["bar"] == "stainless" + + # uses new values for any already given headers + copied = client.copy(default_query={"foo": "stainless"}) + assert _get_params(copied)["foo"] == "stainless" + + # set_default_query + + # completely overrides already set values + copied = client.copy(set_default_query={}) + assert _get_params(copied) == {} + + copied = client.copy(set_default_query={"bar": "Robert"}) + assert _get_params(copied)["bar"] == "Robert" + + with pytest.raises( + ValueError, + # TODO: update + match="`default_query` and `set_default_query` arguments are mutually exclusive", + ): + client.copy(set_default_query={}, default_query={"foo": "Bar"}) + + def test_copy_signature(self) -> None: + # ensure the same parameters that can be passed to the client are defined in the `.copy()` method + init_signature = inspect.signature( + # mypy doesn't like that we access the `__init__` property. + self.client.__init__, # type: ignore[misc] + ) + copy_signature = inspect.signature(self.client.copy) + exclude_params = {"transport", "proxies", "_strict_response_validation"} + + for name in init_signature.parameters.keys(): + if name in exclude_params: + continue + + copy_param = copy_signature.parameters.get(name) + assert copy_param is not None, f"copy() signature is missing the {name} param" + + def test_copy_build_request(self) -> None: + options = FinalRequestOptions(method="get", url="/foo") + + def build_request(options: FinalRequestOptions) -> None: + client = self.client.copy() + client._build_request(options) + + # ensure that the machinery is warmed up before tracing starts. + build_request(options) + gc.collect() + + tracemalloc.start(1000) + + snapshot_before = tracemalloc.take_snapshot() + + ITERATIONS = 10 + for _ in range(ITERATIONS): + build_request(options) + + gc.collect() + snapshot_after = tracemalloc.take_snapshot() + + tracemalloc.stop() + + def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.StatisticDiff) -> None: + if diff.count == 0: + # Avoid false positives by considering only leaks (i.e. allocations that persist). + return + + if diff.count % ITERATIONS != 0: + # Avoid false positives by considering only leaks that appear per iteration. + return + + for frame in diff.traceback: + if any( + frame.filename.endswith(fragment) + for fragment in [ + # to_raw_response_wrapper leaks through the @functools.wraps() decorator. + # + # removing the decorator fixes the leak for reasons we don't understand. + "kernel/_legacy_response.py", + "kernel/_response.py", + # pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason. + "kernel/_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 = Kernel(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 = Kernel( + 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 = Kernel( + 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 = Kernel( + 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: + Kernel( + 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 = Kernel( + 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 = Kernel( + 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 = Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("Authorization") == f"Bearer {api_key}" + + with pytest.raises(KernelError): + with update_env(**{"KERNEL_API_KEY": Omit()}): + client2 = Kernel(base_url=base_url, api_key=None, _strict_response_validation=True) + _ = client2 + + def test_default_query_option(self) -> None: + client = Kernel( + 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: Kernel) -> None: + request = client._build_request( + FinalRequestOptions.construct( + method="get", + url="/foo", + headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"}, + json_data={"array": ["foo", "bar"]}, + files=[("foo.txt", b"hello world")], + ) + ) + + assert request.read().split(b"\r\n") == [ + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="array[]"', + b"", + b"foo", + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="array[]"', + b"", + b"bar", + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="foo.txt"; filename="upload"', + b"Content-Type: application/octet-stream", + b"", + b"hello world", + b"--6b7ba517decee4a450543ea6ae821c82--", + b"", + ] + + @pytest.mark.respx(base_url=base_url) + def test_basic_union_response(self, respx_mock: MockRouter) -> None: + class Model1(BaseModel): + name: str + + class Model2(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model2) + assert response.foo == "bar" + + @pytest.mark.respx(base_url=base_url) + def test_union_response_different_types(self, respx_mock: MockRouter) -> None: + """Union of objects with the same field name using a different type""" + + class Model1(BaseModel): + foo: int + + class Model2(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model2) + assert response.foo == "bar" + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1})) + + response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model1) + assert response.foo == 1 + + @pytest.mark.respx(base_url=base_url) + def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None: + """ + Response that sets Content-Type to something other than application/json but returns json data + """ + + class Model(BaseModel): + foo: int + + respx_mock.get("/foo").mock( + return_value=httpx.Response( + 200, + content=json.dumps({"foo": 2}), + headers={"Content-Type": "application/text"}, + ) + ) + + response = self.client.get("/foo", cast_to=Model) + assert isinstance(response, Model) + assert response.foo == 2 + + def test_base_url_setter(self) -> None: + client = Kernel(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(KERNEL_BASE_URL="http://localhost:5000/from/env"): + client = Kernel(api_key=api_key, _strict_response_validation=True) + assert client.base_url == "http://localhost:5000/from/env/" + + @pytest.mark.parametrize( + "client", + [ + Kernel(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), + Kernel( + 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: Kernel) -> 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", + [ + Kernel(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), + Kernel( + 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: Kernel) -> 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", + [ + Kernel(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), + Kernel( + 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: Kernel) -> 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 = Kernel(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 = Kernel(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"): + Kernel(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 = Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) + + with pytest.raises(APIResponseValidationError): + strict_client.get("/foo", cast_to=Model) + + client = Kernel(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 = Kernel(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("kernel._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: + respx_mock.post("/apps/deploy").mock(side_effect=httpx.TimeoutException("Test timeout error")) + + with pytest.raises(APITimeoutError): + self.client.post( + "/apps/deploy", + body=cast( + object, + maybe_transform( + dict(app_name="REPLACE_ME", file=b"REPLACE_ME", version="REPLACE_ME"), AppDeployParams + ), + ), + cast_to=httpx.Response, + options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, + ) + + assert _get_open_connections(self.client) == 0 + + @mock.patch("kernel._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: + respx_mock.post("/apps/deploy").mock(return_value=httpx.Response(500)) + + with pytest.raises(APIStatusError): + self.client.post( + "/apps/deploy", + body=cast( + object, + maybe_transform( + dict(app_name="REPLACE_ME", file=b"REPLACE_ME", version="REPLACE_ME"), AppDeployParams + ), + ), + cast_to=httpx.Response, + options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, + ) + + assert _get_open_connections(self.client) == 0 + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("kernel._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: Kernel, + 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("/apps/deploy").mock(side_effect=retry_handler) + + response = client.apps.with_raw_response.deploy( + app_name="my-awesome-app", file=b"raw file contents", version="1.0.0" + ) + + 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("kernel._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + def test_omit_retry_count_header( + self, client: Kernel, 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("/apps/deploy").mock(side_effect=retry_handler) + + response = client.apps.with_raw_response.deploy( + app_name="my-awesome-app", + file=b"raw file contents", + version="1.0.0", + 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("kernel._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + def test_overwrite_retry_count_header( + self, client: Kernel, 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("/apps/deploy").mock(side_effect=retry_handler) + + response = client.apps.with_raw_response.deploy( + app_name="my-awesome-app", + file=b"raw file contents", + version="1.0.0", + extra_headers={"x-stainless-retry-count": "42"}, + ) + + assert response.http_request.headers.get("x-stainless-retry-count") == "42" + + +class TestAsyncKernel: + client = AsyncKernel(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 = AsyncKernel( + 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 = AsyncKernel( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} + ) + assert _get_params(client)["foo"] == "bar" + + # does not override the already given value when not specified + copied = client.copy() + assert _get_params(copied)["foo"] == "bar" + + # merges already given params + copied = client.copy(default_query={"bar": "stainless"}) + params = _get_params(copied) + assert params["foo"] == "bar" + assert params["bar"] == "stainless" + + # uses new values for any already given headers + copied = client.copy(default_query={"foo": "stainless"}) + assert _get_params(copied)["foo"] == "stainless" + + # set_default_query + + # completely overrides already set values + copied = client.copy(set_default_query={}) + assert _get_params(copied) == {} + + copied = client.copy(set_default_query={"bar": "Robert"}) + assert _get_params(copied)["bar"] == "Robert" + + with pytest.raises( + ValueError, + # TODO: update + match="`default_query` and `set_default_query` arguments are mutually exclusive", + ): + client.copy(set_default_query={}, default_query={"foo": "Bar"}) + + def test_copy_signature(self) -> None: + # ensure the same parameters that can be passed to the client are defined in the `.copy()` method + init_signature = inspect.signature( + # mypy doesn't like that we access the `__init__` property. + self.client.__init__, # type: ignore[misc] + ) + copy_signature = inspect.signature(self.client.copy) + exclude_params = {"transport", "proxies", "_strict_response_validation"} + + for name in init_signature.parameters.keys(): + if name in exclude_params: + continue + + copy_param = copy_signature.parameters.get(name) + assert copy_param is not None, f"copy() signature is missing the {name} param" + + def test_copy_build_request(self) -> None: + options = FinalRequestOptions(method="get", url="/foo") + + def build_request(options: FinalRequestOptions) -> None: + client = self.client.copy() + client._build_request(options) + + # ensure that the machinery is warmed up before tracing starts. + build_request(options) + gc.collect() + + tracemalloc.start(1000) + + snapshot_before = tracemalloc.take_snapshot() + + ITERATIONS = 10 + for _ in range(ITERATIONS): + build_request(options) + + gc.collect() + snapshot_after = tracemalloc.take_snapshot() + + tracemalloc.stop() + + def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.StatisticDiff) -> None: + if diff.count == 0: + # Avoid false positives by considering only leaks (i.e. allocations that persist). + return + + if diff.count % ITERATIONS != 0: + # Avoid false positives by considering only leaks that appear per iteration. + return + + for frame in diff.traceback: + if any( + frame.filename.endswith(fragment) + for fragment in [ + # to_raw_response_wrapper leaks through the @functools.wraps() decorator. + # + # removing the decorator fixes the leak for reasons we don't understand. + "kernel/_legacy_response.py", + "kernel/_response.py", + # pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason. + "kernel/_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 = AsyncKernel( + 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 = AsyncKernel( + 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 = AsyncKernel( + 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 = AsyncKernel( + 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: + AsyncKernel( + 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 = AsyncKernel( + 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 = AsyncKernel( + 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 = AsyncKernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("Authorization") == f"Bearer {api_key}" + + with pytest.raises(KernelError): + with update_env(**{"KERNEL_API_KEY": Omit()}): + client2 = AsyncKernel(base_url=base_url, api_key=None, _strict_response_validation=True) + _ = client2 + + def test_default_query_option(self) -> None: + client = AsyncKernel( + 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: AsyncKernel) -> None: + request = async_client._build_request( + FinalRequestOptions.construct( + method="get", + url="/foo", + headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"}, + json_data={"array": ["foo", "bar"]}, + files=[("foo.txt", b"hello world")], + ) + ) + + assert request.read().split(b"\r\n") == [ + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="array[]"', + b"", + b"foo", + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="array[]"', + b"", + b"bar", + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="foo.txt"; filename="upload"', + b"Content-Type: application/octet-stream", + b"", + b"hello world", + b"--6b7ba517decee4a450543ea6ae821c82--", + b"", + ] + + @pytest.mark.respx(base_url=base_url) + async def test_basic_union_response(self, respx_mock: MockRouter) -> None: + class Model1(BaseModel): + name: str + + class Model2(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model2) + assert response.foo == "bar" + + @pytest.mark.respx(base_url=base_url) + async def test_union_response_different_types(self, respx_mock: MockRouter) -> None: + """Union of objects with the same field name using a different type""" + + class Model1(BaseModel): + foo: int + + class Model2(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model2) + assert response.foo == "bar" + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1})) + + response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model1) + assert response.foo == 1 + + @pytest.mark.respx(base_url=base_url) + async def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None: + """ + Response that sets Content-Type to something other than application/json but returns json data + """ + + class Model(BaseModel): + foo: int + + respx_mock.get("/foo").mock( + return_value=httpx.Response( + 200, + content=json.dumps({"foo": 2}), + headers={"Content-Type": "application/text"}, + ) + ) + + response = await self.client.get("/foo", cast_to=Model) + assert isinstance(response, Model) + assert response.foo == 2 + + def test_base_url_setter(self) -> None: + client = AsyncKernel( + 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(KERNEL_BASE_URL="http://localhost:5000/from/env"): + client = AsyncKernel(api_key=api_key, _strict_response_validation=True) + assert client.base_url == "http://localhost:5000/from/env/" + + @pytest.mark.parametrize( + "client", + [ + AsyncKernel( + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True + ), + AsyncKernel( + 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: AsyncKernel) -> 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", + [ + AsyncKernel( + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True + ), + AsyncKernel( + 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: AsyncKernel) -> 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", + [ + AsyncKernel( + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True + ), + AsyncKernel( + 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: AsyncKernel) -> 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 = AsyncKernel(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 = AsyncKernel(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"): + AsyncKernel( + 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 = AsyncKernel(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 = AsyncKernel(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 = AsyncKernel(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("kernel._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: + respx_mock.post("/apps/deploy").mock(side_effect=httpx.TimeoutException("Test timeout error")) + + with pytest.raises(APITimeoutError): + await self.client.post( + "/apps/deploy", + body=cast( + object, + maybe_transform( + dict(app_name="REPLACE_ME", file=b"REPLACE_ME", version="REPLACE_ME"), AppDeployParams + ), + ), + cast_to=httpx.Response, + options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, + ) + + assert _get_open_connections(self.client) == 0 + + @mock.patch("kernel._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: + respx_mock.post("/apps/deploy").mock(return_value=httpx.Response(500)) + + with pytest.raises(APIStatusError): + await self.client.post( + "/apps/deploy", + body=cast( + object, + maybe_transform( + dict(app_name="REPLACE_ME", file=b"REPLACE_ME", version="REPLACE_ME"), AppDeployParams + ), + ), + cast_to=httpx.Response, + options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, + ) + + assert _get_open_connections(self.client) == 0 + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("kernel._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: AsyncKernel, + 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("/apps/deploy").mock(side_effect=retry_handler) + + response = await client.apps.with_raw_response.deploy( + app_name="my-awesome-app", file=b"raw file contents", version="1.0.0" + ) + + 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("kernel._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: AsyncKernel, 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("/apps/deploy").mock(side_effect=retry_handler) + + response = await client.apps.with_raw_response.deploy( + app_name="my-awesome-app", + file=b"raw file contents", + version="1.0.0", + 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("kernel._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: AsyncKernel, 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("/apps/deploy").mock(side_effect=retry_handler) + + response = await client.apps.with_raw_response.deploy( + app_name="my-awesome-app", + file=b"raw file contents", + version="1.0.0", + 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 kernel._utils import asyncify + from kernel._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) diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py new file mode 100644 index 00000000..83b72cd9 --- /dev/null +++ b/tests/test_deepcopy.py @@ -0,0 +1,58 @@ +from kernel._utils import deepcopy_minimal + + +def assert_different_identities(obj1: object, obj2: object) -> None: + assert obj1 == obj2 + assert id(obj1) != id(obj2) + + +def test_simple_dict() -> None: + obj1 = {"foo": "bar"} + obj2 = deepcopy_minimal(obj1) + assert_different_identities(obj1, obj2) + + +def test_nested_dict() -> None: + obj1 = {"foo": {"bar": True}} + obj2 = deepcopy_minimal(obj1) + assert_different_identities(obj1, obj2) + assert_different_identities(obj1["foo"], obj2["foo"]) + + +def test_complex_nested_dict() -> None: + obj1 = {"foo": {"bar": [{"hello": "world"}]}} + obj2 = deepcopy_minimal(obj1) + assert_different_identities(obj1, obj2) + assert_different_identities(obj1["foo"], obj2["foo"]) + assert_different_identities(obj1["foo"]["bar"], obj2["foo"]["bar"]) + assert_different_identities(obj1["foo"]["bar"][0], obj2["foo"]["bar"][0]) + + +def test_simple_list() -> None: + obj1 = ["a", "b", "c"] + obj2 = deepcopy_minimal(obj1) + assert_different_identities(obj1, obj2) + + +def test_nested_list() -> None: + obj1 = ["a", [1, 2, 3]] + obj2 = deepcopy_minimal(obj1) + assert_different_identities(obj1, obj2) + assert_different_identities(obj1[1], obj2[1]) + + +class MyObject: ... + + +def test_ignores_other_types() -> None: + # custom classes + my_obj = MyObject() + obj1 = {"foo": my_obj} + obj2 = deepcopy_minimal(obj1) + assert_different_identities(obj1, obj2) + assert obj1["foo"] is my_obj + + # tuples + obj3 = ("a", "b") + obj4 = deepcopy_minimal(obj3) + assert obj3 is obj4 diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py new file mode 100644 index 00000000..e5cf4a1b --- /dev/null +++ b/tests/test_extract_files.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +from typing import Sequence + +import pytest + +from kernel._types import FileTypes +from kernel._utils import extract_files + + +def test_removes_files_from_input() -> None: + query = {"foo": "bar"} + assert extract_files(query, paths=[]) == [] + assert query == {"foo": "bar"} + + query2 = {"foo": b"Bar", "hello": "world"} + assert extract_files(query2, paths=[["foo"]]) == [("foo", b"Bar")] + assert query2 == {"hello": "world"} + + query3 = {"foo": {"foo": {"bar": b"Bar"}}, "hello": "world"} + assert extract_files(query3, paths=[["foo", "foo", "bar"]]) == [("foo[foo][bar]", b"Bar")] + assert query3 == {"foo": {"foo": {}}, "hello": "world"} + + query4 = {"foo": {"bar": b"Bar", "baz": "foo"}, "hello": "world"} + assert extract_files(query4, paths=[["foo", "bar"]]) == [("foo[bar]", b"Bar")] + assert query4 == {"hello": "world", "foo": {"baz": "foo"}} + + +def test_multiple_files() -> None: + query = {"documents": [{"file": b"My first file"}, {"file": b"My second file"}]} + assert extract_files(query, paths=[["documents", "", "file"]]) == [ + ("documents[][file]", b"My first file"), + ("documents[][file]", b"My second file"), + ] + assert query == {"documents": [{}, {}]} + + +@pytest.mark.parametrize( + "query,paths,expected", + [ + [ + {"foo": {"bar": "baz"}}, + [["foo", "", "bar"]], + [], + ], + [ + {"foo": ["bar", "baz"]}, + [["foo", "bar"]], + [], + ], + [ + {"foo": {"bar": "baz"}}, + [["foo", "foo"]], + [], + ], + ], + ids=["dict expecting array", "array expecting dict", "unknown keys"], +) +def test_ignores_incorrect_paths( + query: dict[str, object], + paths: Sequence[Sequence[str]], + expected: list[tuple[str, FileTypes]], +) -> None: + assert extract_files(query, paths=paths) == expected diff --git a/tests/test_files.py b/tests/test_files.py new file mode 100644 index 00000000..62b874f0 --- /dev/null +++ b/tests/test_files.py @@ -0,0 +1,51 @@ +from pathlib import Path + +import anyio +import pytest +from dirty_equals import IsDict, IsList, IsBytes, IsTuple + +from kernel._files import to_httpx_files, async_to_httpx_files + +readme_path = Path(__file__).parent.parent.joinpath("README.md") + + +def test_pathlib_includes_file_name() -> None: + result = to_httpx_files({"file": readme_path}) + print(result) + assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) + + +def test_tuple_input() -> None: + result = to_httpx_files([("file", readme_path)]) + print(result) + assert result == IsList(IsTuple("file", IsTuple("README.md", IsBytes()))) + + +@pytest.mark.asyncio +async def test_async_pathlib_includes_file_name() -> None: + result = await async_to_httpx_files({"file": readme_path}) + print(result) + assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) + + +@pytest.mark.asyncio +async def test_async_supports_anyio_path() -> None: + result = await async_to_httpx_files({"file": anyio.Path(readme_path)}) + print(result) + assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) + + +@pytest.mark.asyncio +async def test_async_tuple_input() -> None: + result = await async_to_httpx_files([("file", readme_path)]) + print(result) + assert result == IsList(IsTuple("file", IsTuple("README.md", IsBytes()))) + + +def test_string_not_allowed() -> None: + with pytest.raises(TypeError, match="Expected file types input to be a FileContent type or to be a tuple"): + to_httpx_files( + { + "file": "foo", # type: ignore + } + ) diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 00000000..4f412178 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,891 @@ +import json +from typing import 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 kernel._utils import PropertyInfo +from kernel._compat import PYDANTIC_V2, parse_obj, model_dump, model_json +from kernel._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) diff --git a/tests/test_qs.py b/tests/test_qs.py new file mode 100644 index 00000000..78ae641d --- /dev/null +++ b/tests/test_qs.py @@ -0,0 +1,78 @@ +from typing import Any, cast +from functools import partial +from urllib.parse import unquote + +import pytest + +from kernel._qs import Querystring, stringify + + +def test_empty() -> None: + assert stringify({}) == "" + assert stringify({"a": {}}) == "" + assert stringify({"a": {"b": {"c": {}}}}) == "" + + +def test_basic() -> None: + assert stringify({"a": 1}) == "a=1" + assert stringify({"a": "b"}) == "a=b" + assert stringify({"a": True}) == "a=true" + assert stringify({"a": False}) == "a=false" + assert stringify({"a": 1.23456}) == "a=1.23456" + assert stringify({"a": None}) == "" + + +@pytest.mark.parametrize("method", ["class", "function"]) +def test_nested_dotted(method: str) -> None: + if method == "class": + serialise = Querystring(nested_format="dots").stringify + else: + serialise = partial(stringify, nested_format="dots") + + assert unquote(serialise({"a": {"b": "c"}})) == "a.b=c" + assert unquote(serialise({"a": {"b": "c", "d": "e", "f": "g"}})) == "a.b=c&a.d=e&a.f=g" + assert unquote(serialise({"a": {"b": {"c": {"d": "e"}}}})) == "a.b.c.d=e" + assert unquote(serialise({"a": {"b": True}})) == "a.b=true" + + +def test_nested_brackets() -> None: + assert unquote(stringify({"a": {"b": "c"}})) == "a[b]=c" + assert unquote(stringify({"a": {"b": "c", "d": "e", "f": "g"}})) == "a[b]=c&a[d]=e&a[f]=g" + assert unquote(stringify({"a": {"b": {"c": {"d": "e"}}}})) == "a[b][c][d]=e" + assert unquote(stringify({"a": {"b": True}})) == "a[b]=true" + + +@pytest.mark.parametrize("method", ["class", "function"]) +def test_array_comma(method: str) -> None: + if method == "class": + serialise = Querystring(array_format="comma").stringify + else: + serialise = partial(stringify, array_format="comma") + + assert unquote(serialise({"in": ["foo", "bar"]})) == "in=foo,bar" + assert unquote(serialise({"a": {"b": [True, False]}})) == "a[b]=true,false" + assert unquote(serialise({"a": {"b": [True, False, None, True]}})) == "a[b]=true,false,true" + + +def test_array_repeat() -> None: + assert unquote(stringify({"in": ["foo", "bar"]})) == "in=foo&in=bar" + assert unquote(stringify({"a": {"b": [True, False]}})) == "a[b]=true&a[b]=false" + assert unquote(stringify({"a": {"b": [True, False, None, True]}})) == "a[b]=true&a[b]=false&a[b]=true" + assert unquote(stringify({"in": ["foo", {"b": {"c": ["d", "e"]}}]})) == "in=foo&in[b][c]=d&in[b][c]=e" + + +@pytest.mark.parametrize("method", ["class", "function"]) +def test_array_brackets(method: str) -> None: + if method == "class": + serialise = Querystring(array_format="brackets").stringify + else: + serialise = partial(stringify, array_format="brackets") + + assert unquote(serialise({"in": ["foo", "bar"]})) == "in[]=foo&in[]=bar" + assert unquote(serialise({"a": {"b": [True, False]}})) == "a[b][]=true&a[b][]=false" + assert unquote(serialise({"a": {"b": [True, False, None, True]}})) == "a[b][]=true&a[b][]=false&a[b][]=true" + + +def test_unknown_array_format() -> None: + with pytest.raises(NotImplementedError, match="Unknown array_format value: foo, choose from comma, repeat"): + stringify({"a": ["foo", "bar"]}, array_format=cast(Any, "foo")) diff --git a/tests/test_required_args.py b/tests/test_required_args.py new file mode 100644 index 00000000..7186db81 --- /dev/null +++ b/tests/test_required_args.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +import pytest + +from kernel._utils import required_args + + +def test_too_many_positional_params() -> None: + @required_args(["a"]) + def foo(a: str | None = None) -> str | None: + return a + + with pytest.raises(TypeError, match=r"foo\(\) takes 1 argument\(s\) but 2 were given"): + foo("a", "b") # type: ignore + + +def test_positional_param() -> None: + @required_args(["a"]) + def foo(a: str | None = None) -> str | None: + return a + + assert foo("a") == "a" + assert foo(None) is None + assert foo(a="b") == "b" + + with pytest.raises(TypeError, match="Missing required argument: 'a'"): + foo() + + +def test_keyword_only_param() -> None: + @required_args(["a"]) + def foo(*, a: str | None = None) -> str | None: + return a + + assert foo(a="a") == "a" + assert foo(a=None) is None + assert foo(a="b") == "b" + + with pytest.raises(TypeError, match="Missing required argument: 'a'"): + foo() + + +def test_multiple_params() -> None: + @required_args(["a", "b", "c"]) + def foo(a: str = "", *, b: str = "", c: str = "") -> str | None: + return f"{a} {b} {c}" + + assert foo(a="a", b="b", c="c") == "a b c" + + error_message = r"Missing required arguments.*" + + with pytest.raises(TypeError, match=error_message): + foo() + + with pytest.raises(TypeError, match=error_message): + foo(a="a") + + with pytest.raises(TypeError, match=error_message): + foo(b="b") + + with pytest.raises(TypeError, match=error_message): + foo(c="c") + + with pytest.raises(TypeError, match=r"Missing required argument: 'a'"): + foo(b="a", c="c") + + with pytest.raises(TypeError, match=r"Missing required argument: 'b'"): + foo("a", c="c") + + +def test_multiple_variants() -> None: + @required_args(["a"], ["b"]) + def foo(*, a: str | None = None, b: str | None = None) -> str | None: + return a if a is not None else b + + assert foo(a="foo") == "foo" + assert foo(b="bar") == "bar" + assert foo(a=None) is None + assert foo(b=None) is None + + # TODO: this error message could probably be improved + with pytest.raises( + TypeError, + match=r"Missing required arguments; Expected either \('a'\) or \('b'\) arguments to be given", + ): + foo() + + +def test_multiple_params_multiple_variants() -> None: + @required_args(["a", "b"], ["c"]) + def foo(*, a: str | None = None, b: str | None = None, c: str | None = None) -> str | None: + if a is not None: + return a + if b is not None: + return b + return c + + error_message = r"Missing required arguments; Expected either \('a' and 'b'\) or \('c'\) arguments to be given" + + with pytest.raises(TypeError, match=error_message): + foo(a="foo") + + with pytest.raises(TypeError, match=error_message): + foo(b="bar") + + with pytest.raises(TypeError, match=error_message): + foo() + + assert foo(a=None, b="bar") == "bar" + assert foo(c=None) is None + assert foo(c="foo") == "foo" diff --git a/tests/test_response.py b/tests/test_response.py new file mode 100644 index 00000000..bf62a9b1 --- /dev/null +++ b/tests/test_response.py @@ -0,0 +1,277 @@ +import json +from typing import Any, List, Union, cast +from typing_extensions import Annotated + +import httpx +import pytest +import pydantic + +from kernel import Kernel, BaseModel, AsyncKernel +from kernel._response import ( + APIResponse, + BaseAPIResponse, + AsyncAPIResponse, + BinaryAPIResponse, + AsyncBinaryAPIResponse, + extract_response_type, +) +from kernel._streaming import Stream +from kernel._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: Kernel) -> 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 kernel import BaseModel`", + ): + response.parse(to=PydanticModel) + + +@pytest.mark.asyncio +async def test_async_response_parse_mismatched_basemodel(async_client: AsyncKernel) -> 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 kernel import BaseModel`", + ): + await response.parse(to=PydanticModel) + + +def test_response_parse_custom_stream(client: Kernel) -> 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: AsyncKernel) -> 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: Kernel) -> 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: AsyncKernel) -> 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: Kernel) -> 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: AsyncKernel) -> 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: Kernel, 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: AsyncKernel, 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: Kernel) -> 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: AsyncKernel) -> None: + response = AsyncAPIResponse( + raw=httpx.Response(200, content=b"foo", headers={"Content-Type": "application/text"}), + client=async_client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + obj = await response.parse(to=cast(Any, Union[CustomModel, OtherModel])) + assert isinstance(obj, str) + assert obj == "foo" diff --git a/tests/test_streaming.py b/tests/test_streaming.py new file mode 100644 index 00000000..4b8e4e48 --- /dev/null +++ b/tests/test_streaming.py @@ -0,0 +1,248 @@ +from __future__ import annotations + +from typing import Iterator, AsyncIterator + +import httpx +import pytest + +from kernel import Kernel, AsyncKernel +from kernel._streaming import Stream, AsyncStream, ServerSentEvent + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_basic(sync: bool, client: Kernel, async_client: AsyncKernel) -> 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: Kernel, async_client: AsyncKernel) -> 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: Kernel, async_client: AsyncKernel) -> 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: Kernel, async_client: AsyncKernel) -> 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: Kernel, async_client: AsyncKernel) -> 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: Kernel, async_client: AsyncKernel) -> 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: Kernel, async_client: AsyncKernel) -> 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: Kernel, async_client: AsyncKernel) -> 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: Kernel, + async_client: AsyncKernel, +) -> 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: Kernel, + async_client: AsyncKernel, +) -> 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: Kernel, + async_client: AsyncKernel, +) -> Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]: + if sync: + return Stream(cast_to=object, client=client, response=httpx.Response(200, content=content))._iter_events() + + return AsyncStream( + cast_to=object, client=async_client, response=httpx.Response(200, content=to_aiter(content)) + )._iter_events() diff --git a/tests/test_transform.py b/tests/test_transform.py new file mode 100644 index 00000000..a418f4f1 --- /dev/null +++ b/tests/test_transform.py @@ -0,0 +1,453 @@ +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 kernel._types import NOT_GIVEN, Base64FileInput +from kernel._utils import ( + PropertyInfo, + transform as _transform, + parse_datetime, + async_transform as _async_transform, +) +from kernel._compat import PYDANTIC_V2 +from kernel._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 new file mode 100644 index 00000000..8c9c8ae3 --- /dev/null +++ b/tests/test_utils/test_proxy.py @@ -0,0 +1,34 @@ +import operator +from typing import Any +from typing_extensions import override + +from kernel._utils import LazyProxy + + +class RecursiveLazyProxy(LazyProxy[Any]): + @override + def __load__(self) -> Any: + return self + + def __call__(self, *_args: Any, **_kwds: Any) -> Any: + raise RuntimeError("This should never be called!") + + +def test_recursive_proxy() -> None: + proxy = RecursiveLazyProxy() + assert repr(proxy) == "RecursiveLazyProxy" + assert str(proxy) == "RecursiveLazyProxy" + assert dir(proxy) == [] + assert type(proxy).__name__ == "RecursiveLazyProxy" + assert type(operator.attrgetter("name.foo.bar.baz")(proxy)).__name__ == "RecursiveLazyProxy" + + +def test_isinstance_does_not_error() -> None: + class AlwaysErrorProxy(LazyProxy[Any]): + @override + def __load__(self) -> Any: + raise RuntimeError("Mocking missing dependency") + + proxy = AlwaysErrorProxy() + assert not isinstance(proxy, dict) + assert isinstance(proxy, LazyProxy) diff --git a/tests/test_utils/test_typing.py b/tests/test_utils/test_typing.py new file mode 100644 index 00000000..3b18d48a --- /dev/null +++ b/tests/test_utils/test_typing.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +from typing import Generic, TypeVar, cast + +from kernel._utils import extract_type_var_from_base + +_T = TypeVar("_T") +_T2 = TypeVar("_T2") +_T3 = TypeVar("_T3") + + +class BaseGeneric(Generic[_T]): ... + + +class SubclassGeneric(BaseGeneric[_T]): ... + + +class BaseGenericMultipleTypeArgs(Generic[_T, _T2, _T3]): ... + + +class SubclassGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T, _T2, _T3]): ... + + +class SubclassDifferentOrderGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T2, _T, _T3]): ... + + +def test_extract_type_var() -> None: + assert ( + extract_type_var_from_base( + BaseGeneric[int], + index=0, + generic_bases=cast("tuple[type, ...]", (BaseGeneric,)), + ) + == int + ) + + +def test_extract_type_var_generic_subclass() -> None: + assert ( + extract_type_var_from_base( + SubclassGeneric[int], + index=0, + generic_bases=cast("tuple[type, ...]", (BaseGeneric,)), + ) + == int + ) + + +def test_extract_type_var_multiple() -> None: + typ = BaseGenericMultipleTypeArgs[int, str, None] + + generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) + assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int + assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str + assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) + + +def test_extract_type_var_generic_subclass_multiple() -> None: + typ = SubclassGenericMultipleTypeArgs[int, str, None] + + generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) + assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int + assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str + assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) + + +def test_extract_type_var_generic_subclass_different_ordering_multiple() -> None: + typ = SubclassDifferentOrderGenericMultipleTypeArgs[int, str, None] + + generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) + assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int + assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str + assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 00000000..d81c8f4b --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,159 @@ +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 kernel._types import Omit, NoneType +from kernel._utils import ( + is_dict, + is_list, + is_list_type, + is_union_type, + extract_type_arg, + is_annotated_type, + is_type_alias_type, +) +from kernel._compat import PYDANTIC_V2, field_outer_type, get_model_fields +from kernel._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) From 551c1c509ff6135f18d88f2cf20ee9dd31e728c0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 8 May 2025 15:12:41 +0000 Subject: [PATCH 002/448] chore: update SDK settings --- .github/workflows/publish-pypi.yml | 31 +++++++++++++ .github/workflows/release-doctor.yml | 21 +++++++++ .release-please-manifest.json | 3 ++ .stats.yml | 2 +- CONTRIBUTING.md | 4 +- README.md | 10 ++--- bin/check-release-environment | 21 +++++++++ pyproject.toml | 6 +-- release-please-config.json | 66 ++++++++++++++++++++++++++++ src/kernel/_files.py | 2 +- src/kernel/_version.py | 2 +- src/kernel/resources/apps.py | 8 ++-- src/kernel/resources/browser.py | 8 ++-- 13 files changed, 163 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/publish-pypi.yml create mode 100644 .github/workflows/release-doctor.yml create mode 100644 .release-please-manifest.json create mode 100644 bin/check-release-environment create mode 100644 release-please-config.json diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml new file mode 100644 index 00000000..120241d9 --- /dev/null +++ b/.github/workflows/publish-pypi.yml @@ -0,0 +1,31 @@ +# This workflow is triggered when a GitHub release is created. +# It can also be run manually to re-publish to PyPI in case it failed for some reason. +# You can run this workflow by navigating to https://www.github.com/onkernel/kernel-python-sdk/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.KERNEL_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml new file mode 100644 index 00000000..5e7787d0 --- /dev/null +++ b/.github/workflows/release-doctor.yml @@ -0,0 +1,21 @@ +name: Release Doctor +on: + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + release_doctor: + name: release doctor + runs-on: ubuntu-latest + if: github.repository == 'onkernel/kernel-python-sdk' && (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.KERNEL_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 00000000..c4762802 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.0.1-alpha.0" +} \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 7bdb6b3d..8a75f87f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 4 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2a9a9232d72f52fc9b2404958857a1b744762356421cf1bffb99db2c24045378.yml openapi_spec_hash: ba3c6823319d99762e7e1c6a624bc2ed -config_hash: e7de12a0c945ca8d537120d0d3b484b2 +config_hash: 70a0338eacd8a9827717b395c0a63d48 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 24d5b0aa..c486484d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,7 +63,7 @@ If you’d like to use the repository from source, you can either install from g To install via git: ```sh -$ pip install git+ssh://git@github.com/stainless-sdks/kernel-python.git +$ pip install git+ssh://git@github.com/onkernel/kernel-python-sdk.git ``` Alternatively, you can build from source and install the wheel file: @@ -121,7 +121,7 @@ the changes aren't made through the automated pipeline, you may want to make rel ### Publish with a GitHub workflow -You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/stainless-sdks/kernel-python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. +You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/onkernel/kernel-python-sdk/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. ### Publish manually diff --git a/README.md b/README.md index 06527eb3..edbe158b 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ The full API of this library can be found in [api.md](api.md). ## Installation ```sh -# install from this staging repo -pip install git+ssh://git@github.com/stainless-sdks/kernel-python.git +# install from the production repo +pip install git+ssh://git@github.com/onkernel/kernel-python-sdk.git ``` > [!NOTE] @@ -249,9 +249,9 @@ app = response.parse() # get the object that `apps.deploy()` would have returne print(app.id) ``` -These methods return an [`APIResponse`](https://github.com/stainless-sdks/kernel-python/tree/main/src/kernel/_response.py) object. +These methods return an [`APIResponse`](https://github.com/onkernel/kernel-python-sdk/tree/main/src/kernel/_response.py) object. -The async client returns an [`AsyncAPIResponse`](https://github.com/stainless-sdks/kernel-python/tree/main/src/kernel/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. +The async client returns an [`AsyncAPIResponse`](https://github.com/onkernel/kernel-python-sdk/tree/main/src/kernel/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. #### `.with_streaming_response` @@ -359,7 +359,7 @@ This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) con We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. -We are keen for your feedback; please open an [issue](https://www.github.com/stainless-sdks/kernel-python/issues) with questions, bugs, or suggestions. +We are keen for your feedback; please open an [issue](https://www.github.com/onkernel/kernel-python-sdk/issues) with questions, bugs, or suggestions. ### Determining the installed version diff --git a/bin/check-release-environment b/bin/check-release-environment new file mode 100644 index 00000000..47a8dcae --- /dev/null +++ b/bin/check-release-environment @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +errors=() + +if [ -z "${PYPI_TOKEN}" ]; then + errors+=("The KERNEL_PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") +fi + +lenErrors=${#errors[@]} + +if [[ lenErrors -gt 0 ]]; then + echo -e "Found the following errors in the release environment:\n" + + for error in "${errors[@]}"; do + echo -e "- $error\n" + done + + exit 1 +fi + +echo "The environment is ready to push releases!" diff --git a/pyproject.toml b/pyproject.toml index b3465a4f..f5fc7117 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,8 +34,8 @@ classifiers = [ ] [project.urls] -Homepage = "https://github.com/stainless-sdks/kernel-python" -Repository = "https://github.com/stainless-sdks/kernel-python" +Homepage = "https://github.com/onkernel/kernel-python-sdk" +Repository = "https://github.com/onkernel/kernel-python-sdk" [tool.rye] @@ -121,7 +121,7 @@ path = "README.md" [[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]] # replace relative links with absolute links pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)' -replacement = '[\1](https://github.com/stainless-sdks/kernel-python/tree/main/\g<2>)' +replacement = '[\1](https://github.com/onkernel/kernel-python-sdk/tree/main/\g<2>)' [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 00000000..942ec08a --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,66 @@ +{ + "packages": { + ".": {} + }, + "$schema": "https://raw.githubusercontent.com/stainless-api/release-please/main/schemas/config.json", + "include-v-in-tag": true, + "include-component-in-tag": false, + "versioning": "prerelease", + "prerelease": true, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": false, + "pull-request-header": "Automated Release PR", + "pull-request-title-pattern": "release: ${version}", + "changelog-sections": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "perf", + "section": "Performance Improvements" + }, + { + "type": "revert", + "section": "Reverts" + }, + { + "type": "chore", + "section": "Chores" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "style", + "section": "Styles" + }, + { + "type": "refactor", + "section": "Refactors" + }, + { + "type": "test", + "section": "Tests", + "hidden": true + }, + { + "type": "build", + "section": "Build System" + }, + { + "type": "ci", + "section": "Continuous Integration", + "hidden": true + } + ], + "release-type": "python", + "extra-files": [ + "src/kernel/_version.py" + ] +} \ No newline at end of file diff --git a/src/kernel/_files.py b/src/kernel/_files.py index df2a05e5..63dab8a5 100644 --- a/src/kernel/_files.py +++ b/src/kernel/_files.py @@ -34,7 +34,7 @@ def assert_is_file_content(obj: object, *, key: str | None = None) -> None: if not is_file_content(obj): prefix = f"Expected entry at `{key}`" if key is not None else f"Expected file input `{obj!r}`" raise RuntimeError( - f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead. See https://github.com/stainless-sdks/kernel-python/tree/main#file-uploads" + f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead. See https://github.com/onkernel/kernel-python-sdk/tree/main#file-uploads" ) from None diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 3d085d7e..9c761c53 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.0.1-alpha.0" +__version__ = "0.0.1-alpha.0" # x-release-please-version diff --git a/src/kernel/resources/apps.py b/src/kernel/resources/apps.py index 74c900c6..30c666c2 100644 --- a/src/kernel/resources/apps.py +++ b/src/kernel/resources/apps.py @@ -32,7 +32,7 @@ def with_raw_response(self) -> AppsResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/kernel-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return AppsResourceWithRawResponse(self) @@ -41,7 +41,7 @@ def with_streaming_response(self) -> AppsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/kernel-python#with_streaming_response + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response """ return AppsResourceWithStreamingResponse(self) @@ -190,7 +190,7 @@ def with_raw_response(self) -> AsyncAppsResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/kernel-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return AsyncAppsResourceWithRawResponse(self) @@ -199,7 +199,7 @@ def with_streaming_response(self) -> AsyncAppsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/kernel-python#with_streaming_response + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response """ return AsyncAppsResourceWithStreamingResponse(self) diff --git a/src/kernel/resources/browser.py b/src/kernel/resources/browser.py index ae79d382..3e3da668 100644 --- a/src/kernel/resources/browser.py +++ b/src/kernel/resources/browser.py @@ -26,7 +26,7 @@ def with_raw_response(self) -> BrowserResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/kernel-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return BrowserResourceWithRawResponse(self) @@ -35,7 +35,7 @@ def with_streaming_response(self) -> BrowserResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/kernel-python#with_streaming_response + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response """ return BrowserResourceWithStreamingResponse(self) @@ -66,7 +66,7 @@ def with_raw_response(self) -> AsyncBrowserResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/kernel-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return AsyncBrowserResourceWithRawResponse(self) @@ -75,7 +75,7 @@ def with_streaming_response(self) -> AsyncBrowserResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/kernel-python#with_streaming_response + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response """ return AsyncBrowserResourceWithStreamingResponse(self) From 5c4cc1440acc54fd454659a85aff71664245e2e7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 8 May 2025 15:14:09 +0000 Subject: [PATCH 003/448] chore: update SDK settings --- .stats.yml | 2 +- README.md | 9 +++------ pyproject.toml | 2 +- requirements-dev.lock | 12 ++++++------ requirements.lock | 12 ++++++------ 5 files changed, 17 insertions(+), 20 deletions(-) diff --git a/.stats.yml b/.stats.yml index 8a75f87f..e0419239 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 4 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2a9a9232d72f52fc9b2404958857a1b744762356421cf1bffb99db2c24045378.yml openapi_spec_hash: ba3c6823319d99762e7e1c6a624bc2ed -config_hash: 70a0338eacd8a9827717b395c0a63d48 +config_hash: 7eb638f72349d12adb152e43c2d785ec diff --git a/README.md b/README.md index edbe158b..b7d6ce2c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Kernel Python API library -[![PyPI version](https://img.shields.io/pypi/v/kernel.svg)](https://pypi.org/project/kernel/) +[![PyPI version](https://img.shields.io/pypi/v/kernel-sdk.svg)](https://pypi.org/project/kernel-sdk/) The Kernel Python library provides convenient access to the Kernel REST API from any Python 3.8+ application. The library includes type definitions for all request params and response fields, @@ -15,13 +15,10 @@ The full API of this library can be found in [api.md](api.md). ## Installation ```sh -# install from the production repo -pip install git+ssh://git@github.com/onkernel/kernel-python-sdk.git +# install from PyPI +pip install --pre kernel-sdk ``` -> [!NOTE] -> Once this package is [published to PyPI](https://app.stainless.com/docs/guides/publish), this will become: `pip install --pre kernel` - ## Usage The full API of this library can be found in [api.md](api.md). diff --git a/pyproject.toml b/pyproject.toml index f5fc7117..7e62a6cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "kernel" +name = "kernel-sdk" version = "0.0.1-alpha.0" description = "The official Python library for the kernel API" dynamic = ["readme"] diff --git a/requirements-dev.lock b/requirements-dev.lock index efd90ead..6af49a05 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -14,7 +14,7 @@ annotated-types==0.6.0 # via pydantic anyio==4.4.0 # via httpx - # via kernel + # via kernel-sdk argcomplete==3.1.2 # via nox certifi==2023.7.22 @@ -26,7 +26,7 @@ dirty-equals==0.6.0 distlib==0.3.7 # via virtualenv distro==1.8.0 - # via kernel + # via kernel-sdk exceptiongroup==1.2.2 # via anyio # via pytest @@ -37,7 +37,7 @@ h11==0.14.0 httpcore==1.0.2 # via httpx httpx==0.28.1 - # via kernel + # via kernel-sdk # via respx idna==3.4 # via anyio @@ -64,7 +64,7 @@ platformdirs==3.11.0 pluggy==1.5.0 # via pytest pydantic==2.10.3 - # via kernel + # via kernel-sdk pydantic-core==2.27.1 # via pydantic pygments==2.18.0 @@ -86,14 +86,14 @@ six==1.16.0 # via python-dateutil sniffio==1.3.0 # via anyio - # via kernel + # via kernel-sdk time-machine==2.9.0 tomli==2.0.2 # via mypy # via pytest typing-extensions==4.12.2 # via anyio - # via kernel + # via kernel-sdk # via mypy # via pydantic # via pydantic-core diff --git a/requirements.lock b/requirements.lock index 40719199..e46d87a3 100644 --- a/requirements.lock +++ b/requirements.lock @@ -14,12 +14,12 @@ annotated-types==0.6.0 # via pydantic anyio==4.4.0 # via httpx - # via kernel + # via kernel-sdk certifi==2023.7.22 # via httpcore # via httpx distro==1.8.0 - # via kernel + # via kernel-sdk exceptiongroup==1.2.2 # via anyio h11==0.14.0 @@ -27,19 +27,19 @@ h11==0.14.0 httpcore==1.0.2 # via httpx httpx==0.28.1 - # via kernel + # via kernel-sdk idna==3.4 # via anyio # via httpx pydantic==2.10.3 - # via kernel + # via kernel-sdk pydantic-core==2.27.1 # via pydantic sniffio==1.3.0 # via anyio - # via kernel + # via kernel-sdk typing-extensions==4.12.2 # via anyio - # via kernel + # via kernel-sdk # via pydantic # via pydantic-core From dd49bf26e68f47e307cb437724ef181ccbcb19b1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 8 May 2025 16:32:27 +0000 Subject: [PATCH 004/448] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index e0419239..b59ce7c7 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 4 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2a9a9232d72f52fc9b2404958857a1b744762356421cf1bffb99db2c24045378.yml openapi_spec_hash: ba3c6823319d99762e7e1c6a624bc2ed -config_hash: 7eb638f72349d12adb152e43c2d785ec +config_hash: e48b09ec26046e2b2ba98ad41ecbaf1c From 3f5658efaed6a0f268180400a5d6b948c7f3be6c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 8 May 2025 16:36:16 +0000 Subject: [PATCH 005/448] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index b59ce7c7..d93970e4 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 4 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2a9a9232d72f52fc9b2404958857a1b744762356421cf1bffb99db2c24045378.yml openapi_spec_hash: ba3c6823319d99762e7e1c6a624bc2ed -config_hash: e48b09ec26046e2b2ba98ad41ecbaf1c +config_hash: 5d8104e64e7d71c412fd8a49600ad33d From f8a87c2976ed87eb2ecc8e0cf0033dcde43f27c3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 8 May 2025 16:38:30 +0000 Subject: [PATCH 006/448] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index d93970e4..49a57d82 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 4 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2a9a9232d72f52fc9b2404958857a1b744762356421cf1bffb99db2c24045378.yml openapi_spec_hash: ba3c6823319d99762e7e1c6a624bc2ed -config_hash: 5d8104e64e7d71c412fd8a49600ad33d +config_hash: 0961a6d918b6ba9b875508988aa408a1 From 6a182ca350a0488c29c61202470cfed6f2bd9fd5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 8 May 2025 16:44:27 +0000 Subject: [PATCH 007/448] codegen metadata --- .stats.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 49a57d82..b66a1bc8 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 4 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2a9a9232d72f52fc9b2404958857a1b744762356421cf1bffb99db2c24045378.yml -openapi_spec_hash: ba3c6823319d99762e7e1c6a624bc2ed -config_hash: 0961a6d918b6ba9b875508988aa408a1 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-e642528081bcfbb78b52900cb9b8b1407a9c7a8653c57ab021a79d4d52585695.yml +openapi_spec_hash: 8d91d1ac100906977531a93b9f4ae380 +config_hash: 49c38455e0bcb05feb11399f9da1fb4f From 79fdee82d5f44b801c3b0c4d95e786afce16b1fc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 8 May 2025 16:50:53 +0000 Subject: [PATCH 008/448] feat(api): update via SDK Studio --- .stats.yml | 2 +- README.md | 4 ++-- pyproject.toml | 2 +- requirements-dev.lock | 12 ++++++------ requirements.lock | 12 ++++++------ 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.stats.yml b/.stats.yml index b66a1bc8..25b168b7 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 4 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-e642528081bcfbb78b52900cb9b8b1407a9c7a8653c57ab021a79d4d52585695.yml openapi_spec_hash: 8d91d1ac100906977531a93b9f4ae380 -config_hash: 49c38455e0bcb05feb11399f9da1fb4f +config_hash: 75c0b894355904e2a91b70445072d4b4 diff --git a/README.md b/README.md index b7d6ce2c..cbec5e4e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Kernel Python API library -[![PyPI version](https://img.shields.io/pypi/v/kernel-sdk.svg)](https://pypi.org/project/kernel-sdk/) +[![PyPI version](https://img.shields.io/pypi/v/kernel.svg)](https://pypi.org/project/kernel/) The Kernel Python library provides convenient access to the Kernel REST API from any Python 3.8+ application. The library includes type definitions for all request params and response fields, @@ -16,7 +16,7 @@ The full API of this library can be found in [api.md](api.md). ```sh # install from PyPI -pip install --pre kernel-sdk +pip install --pre kernel ``` ## Usage diff --git a/pyproject.toml b/pyproject.toml index 7e62a6cb..f5fc7117 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "kernel-sdk" +name = "kernel" version = "0.0.1-alpha.0" description = "The official Python library for the kernel API" dynamic = ["readme"] diff --git a/requirements-dev.lock b/requirements-dev.lock index 6af49a05..efd90ead 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -14,7 +14,7 @@ annotated-types==0.6.0 # via pydantic anyio==4.4.0 # via httpx - # via kernel-sdk + # via kernel argcomplete==3.1.2 # via nox certifi==2023.7.22 @@ -26,7 +26,7 @@ dirty-equals==0.6.0 distlib==0.3.7 # via virtualenv distro==1.8.0 - # via kernel-sdk + # via kernel exceptiongroup==1.2.2 # via anyio # via pytest @@ -37,7 +37,7 @@ h11==0.14.0 httpcore==1.0.2 # via httpx httpx==0.28.1 - # via kernel-sdk + # via kernel # via respx idna==3.4 # via anyio @@ -64,7 +64,7 @@ platformdirs==3.11.0 pluggy==1.5.0 # via pytest pydantic==2.10.3 - # via kernel-sdk + # via kernel pydantic-core==2.27.1 # via pydantic pygments==2.18.0 @@ -86,14 +86,14 @@ six==1.16.0 # via python-dateutil sniffio==1.3.0 # via anyio - # via kernel-sdk + # via kernel time-machine==2.9.0 tomli==2.0.2 # via mypy # via pytest typing-extensions==4.12.2 # via anyio - # via kernel-sdk + # via kernel # via mypy # via pydantic # via pydantic-core diff --git a/requirements.lock b/requirements.lock index e46d87a3..40719199 100644 --- a/requirements.lock +++ b/requirements.lock @@ -14,12 +14,12 @@ annotated-types==0.6.0 # via pydantic anyio==4.4.0 # via httpx - # via kernel-sdk + # via kernel certifi==2023.7.22 # via httpcore # via httpx distro==1.8.0 - # via kernel-sdk + # via kernel exceptiongroup==1.2.2 # via anyio h11==0.14.0 @@ -27,19 +27,19 @@ h11==0.14.0 httpcore==1.0.2 # via httpx httpx==0.28.1 - # via kernel-sdk + # via kernel idna==3.4 # via anyio # via httpx pydantic==2.10.3 - # via kernel-sdk + # via kernel pydantic-core==2.27.1 # via pydantic sniffio==1.3.0 # via anyio - # via kernel-sdk + # via kernel typing-extensions==4.12.2 # via anyio - # via kernel-sdk + # via kernel # via pydantic # via pydantic-core From 9c0ac952f67e628d3f6a7e0eaddc74ecd9316522 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 8 May 2025 16:57:03 +0000 Subject: [PATCH 009/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index c4762802..ba6c3483 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.0.1-alpha.0" + ".": "0.1.0-alpha.1" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index f5fc7117..c3d2b1ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.0.1-alpha.0" +version = "0.1.0-alpha.1" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 9c761c53..eb6a1d3a 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.0.1-alpha.0" # x-release-please-version +__version__ = "0.1.0-alpha.1" # x-release-please-version From c128170e6fffd2f900b114b958dd79172e076f8c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 9 May 2025 15:39:28 +0000 Subject: [PATCH 010/448] feat(api): update via SDK Studio --- .stats.yml | 4 ++-- src/kernel/resources/apps.py | 8 ++++++++ src/kernel/types/app_invoke_params.py | 3 +++ src/kernel/types/app_invoke_response.py | 8 +++++++- tests/api_resources/test_apps.py | 6 ++++++ 5 files changed, 26 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 25b168b7..f654ed7c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 4 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-e642528081bcfbb78b52900cb9b8b1407a9c7a8653c57ab021a79d4d52585695.yml -openapi_spec_hash: 8d91d1ac100906977531a93b9f4ae380 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-d168b58fcf39dbd0458d132091793d3e2d0930070b7dda2d5f7f1baff20dd31b.yml +openapi_spec_hash: b7e0fd7ee1656d7dbad57209d1584d92 config_hash: 75c0b894355904e2a91b70445072d4b4 diff --git a/src/kernel/resources/apps.py b/src/kernel/resources/apps.py index 30c666c2..997a27f0 100644 --- a/src/kernel/resources/apps.py +++ b/src/kernel/resources/apps.py @@ -105,6 +105,7 @@ def deploy( def invoke( self, *, + action_name: str, app_name: str, payload: object, version: str, @@ -119,6 +120,8 @@ def invoke( Invoke an application Args: + action_name: Name of the action to invoke + app_name: Name of the application payload: Input data for the application @@ -137,6 +140,7 @@ def invoke( "/apps/invoke", body=maybe_transform( { + "action_name": action_name, "app_name": app_name, "payload": payload, "version": version, @@ -263,6 +267,7 @@ async def deploy( async def invoke( self, *, + action_name: str, app_name: str, payload: object, version: str, @@ -277,6 +282,8 @@ async def invoke( Invoke an application Args: + action_name: Name of the action to invoke + app_name: Name of the application payload: Input data for the application @@ -295,6 +302,7 @@ async def invoke( "/apps/invoke", body=await async_maybe_transform( { + "action_name": action_name, "app_name": app_name, "payload": payload, "version": version, diff --git a/src/kernel/types/app_invoke_params.py b/src/kernel/types/app_invoke_params.py index 773fd459..414da988 100644 --- a/src/kernel/types/app_invoke_params.py +++ b/src/kernel/types/app_invoke_params.py @@ -10,6 +10,9 @@ class AppInvokeParams(TypedDict, total=False): + action_name: Required[Annotated[str, PropertyInfo(alias="actionName")]] + """Name of the action to invoke""" + app_name: Required[Annotated[str, PropertyInfo(alias="appName")]] """Name of the application""" diff --git a/src/kernel/types/app_invoke_response.py b/src/kernel/types/app_invoke_response.py index 801ec5cc..e76a9fd5 100644 --- a/src/kernel/types/app_invoke_response.py +++ b/src/kernel/types/app_invoke_response.py @@ -1,5 +1,8 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from typing import Optional +from typing_extensions import Literal + from .._models import BaseModel __all__ = ["AppInvokeResponse"] @@ -9,5 +12,8 @@ class AppInvokeResponse(BaseModel): id: str """ID of the invocation""" - status: str + status: Literal["QUEUED", "RUNNING", "SUCCEEDED", "FAILED"] """Status of the invocation""" + + output: Optional[str] = None + """Output from the invocation (if available)""" diff --git a/tests/api_resources/test_apps.py b/tests/api_resources/test_apps.py index 7f086b59..26d0ef11 100644 --- a/tests/api_resources/test_apps.py +++ b/tests/api_resources/test_apps.py @@ -76,6 +76,7 @@ def test_streaming_response_deploy(self, client: Kernel) -> None: @parametrize def test_method_invoke(self, client: Kernel) -> None: app = client.apps.invoke( + action_name="analyze", app_name="my-awesome-app", payload='{ "data": "example input" }', version="1.0.0", @@ -86,6 +87,7 @@ def test_method_invoke(self, client: Kernel) -> None: @parametrize def test_raw_response_invoke(self, client: Kernel) -> None: response = client.apps.with_raw_response.invoke( + action_name="analyze", app_name="my-awesome-app", payload='{ "data": "example input" }', version="1.0.0", @@ -100,6 +102,7 @@ def test_raw_response_invoke(self, client: Kernel) -> None: @parametrize def test_streaming_response_invoke(self, client: Kernel) -> None: with client.apps.with_streaming_response.invoke( + action_name="analyze", app_name="my-awesome-app", payload='{ "data": "example input" }', version="1.0.0", @@ -213,6 +216,7 @@ async def test_streaming_response_deploy(self, async_client: AsyncKernel) -> Non @parametrize async def test_method_invoke(self, async_client: AsyncKernel) -> None: app = await async_client.apps.invoke( + action_name="analyze", app_name="my-awesome-app", payload='{ "data": "example input" }', version="1.0.0", @@ -223,6 +227,7 @@ async def test_method_invoke(self, async_client: AsyncKernel) -> None: @parametrize async def test_raw_response_invoke(self, async_client: AsyncKernel) -> None: response = await async_client.apps.with_raw_response.invoke( + action_name="analyze", app_name="my-awesome-app", payload='{ "data": "example input" }', version="1.0.0", @@ -237,6 +242,7 @@ async def test_raw_response_invoke(self, async_client: AsyncKernel) -> None: @parametrize async def test_streaming_response_invoke(self, async_client: AsyncKernel) -> None: async with async_client.apps.with_streaming_response.invoke( + action_name="analyze", app_name="my-awesome-app", payload='{ "data": "example input" }', version="1.0.0", From b05c100146886685c5fc687213793bb41eeed0ee Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 9 May 2025 15:43:32 +0000 Subject: [PATCH 011/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index ba6c3483..f14b480a 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.1" + ".": "0.1.0-alpha.2" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index c3d2b1ea..c4a4017a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.1.0-alpha.1" +version = "0.1.0-alpha.2" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index eb6a1d3a..ed7a6fd8 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.1.0-alpha.1" # x-release-please-version +__version__ = "0.1.0-alpha.2" # x-release-please-version From f4f727b17c44508fcd343df7a57a97ba0433ca39 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 02:55:13 +0000 Subject: [PATCH 012/448] fix(package): support direct resource imports --- src/kernel/__init__.py | 5 +++++ src/kernel/_utils/_resources_proxy.py | 24 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 src/kernel/_utils/_resources_proxy.py diff --git a/src/kernel/__init__.py b/src/kernel/__init__.py index 1093761b..2a6614ed 100644 --- a/src/kernel/__init__.py +++ b/src/kernel/__init__.py @@ -1,5 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +import typing as _t + from . import types from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes from ._utils import file_from_path @@ -68,6 +70,9 @@ "DefaultAsyncHttpxClient", ] +if not _t.TYPE_CHECKING: + from ._utils._resources_proxy import resources as resources + _setup_logging() # Update the __module__ attribute for exported symbols so that diff --git a/src/kernel/_utils/_resources_proxy.py b/src/kernel/_utils/_resources_proxy.py new file mode 100644 index 00000000..006a6390 --- /dev/null +++ b/src/kernel/_utils/_resources_proxy.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from typing import Any +from typing_extensions import override + +from ._proxy import LazyProxy + + +class ResourcesProxy(LazyProxy[Any]): + """A proxy for the `kernel.resources` module. + + This is used so that we can lazily import `kernel.resources` only when + needed *and* so that users can just import `kernel` and reference `kernel.resources` + """ + + @override + def __load__(self) -> Any: + import importlib + + mod = importlib.import_module("kernel.resources") + return mod + + +resources = ResourcesProxy().__as_proxied__() From ac74e78ecb2f58d6b6936ebc2594114363454e32 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 11:25:42 +0000 Subject: [PATCH 013/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index f14b480a..aaf968a1 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.2" + ".": "0.1.0-alpha.3" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index c4a4017a..3c2101a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.1.0-alpha.2" +version = "0.1.0-alpha.3" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index ed7a6fd8..5f681edd 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.1.0-alpha.2" # x-release-please-version +__version__ = "0.1.0-alpha.3" # x-release-please-version From d8eca97e0881851784b7efaf98c0372aeeda4555 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 19:03:24 +0000 Subject: [PATCH 014/448] feat(api): update via SDK Studio --- .stats.yml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index f654ed7c..3b0a5f22 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 4 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-d168b58fcf39dbd0458d132091793d3e2d0930070b7dda2d5f7f1baff20dd31b.yml openapi_spec_hash: b7e0fd7ee1656d7dbad57209d1584d92 -config_hash: 75c0b894355904e2a91b70445072d4b4 +config_hash: c2bc5253d8afd6d67e031f73353c9b22 diff --git a/README.md b/README.md index cbec5e4e..0e3afec0 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ It is generated with [Stainless](https://www.stainless.com/). ## Documentation -The full API of this library can be found in [api.md](api.md). +The REST API documentation can be found on [docs.onkernel.com](https://docs.onkernel.com). The full API of this library can be found in [api.md](api.md). ## Installation From 9a19d60dd2665418c46873b268f39524fad8b79e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 19:13:04 +0000 Subject: [PATCH 015/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index aaf968a1..b56c3d0b 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.3" + ".": "0.1.0-alpha.4" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 3c2101a1..8764ccde 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.1.0-alpha.3" +version = "0.1.0-alpha.4" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 5f681edd..f5e84303 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.1.0-alpha.3" # x-release-please-version +__version__ = "0.1.0-alpha.4" # x-release-please-version From b2a24a72cdf665270835caf41fe385e7d8a8b026 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 19:20:26 +0000 Subject: [PATCH 016/448] feat(api): update via SDK Studio --- .stats.yml | 2 +- README.md | 4 ++ src/kernel/__init__.py | 14 ++++++- src/kernel/_client.py | 93 ++++++++++++++++++++++++++++++++++++------ tests/test_client.py | 18 ++++++++ 5 files changed, 116 insertions(+), 15 deletions(-) diff --git a/.stats.yml b/.stats.yml index 3b0a5f22..1a7891f2 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 4 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-d168b58fcf39dbd0458d132091793d3e2d0930070b7dda2d5f7f1baff20dd31b.yml openapi_spec_hash: b7e0fd7ee1656d7dbad57209d1584d92 -config_hash: c2bc5253d8afd6d67e031f73353c9b22 +config_hash: 2d282609080a6011e3f6222451f72237 diff --git a/README.md b/README.md index 0e3afec0..ab4e7655 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ from kernel import Kernel client = Kernel( api_key=os.environ.get("KERNEL_API_KEY"), # This is the default and can be omitted + # defaults to "production". + environment="development", ) response = client.apps.deploy( @@ -55,6 +57,8 @@ from kernel import AsyncKernel client = AsyncKernel( api_key=os.environ.get("KERNEL_API_KEY"), # This is the default and can be omitted + # defaults to "production". + environment="development", ) diff --git a/src/kernel/__init__.py b/src/kernel/__init__.py index 2a6614ed..4c0f2540 100644 --- a/src/kernel/__init__.py +++ b/src/kernel/__init__.py @@ -5,7 +5,18 @@ from . import types from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes from ._utils import file_from_path -from ._client import Client, Kernel, Stream, Timeout, Transport, AsyncClient, AsyncKernel, AsyncStream, RequestOptions +from ._client import ( + ENVIRONMENTS, + Client, + Kernel, + Stream, + Timeout, + Transport, + AsyncClient, + AsyncKernel, + AsyncStream, + RequestOptions, +) from ._models import BaseModel from ._version import __title__, __version__ from ._response import APIResponse as APIResponse, AsyncAPIResponse as AsyncAPIResponse @@ -61,6 +72,7 @@ "AsyncStream", "Kernel", "AsyncKernel", + "ENVIRONMENTS", "file_from_path", "BaseModel", "DEFAULT_TIMEOUT", diff --git a/src/kernel/_client.py b/src/kernel/_client.py index aa9f2279..28871bad 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -3,8 +3,8 @@ from __future__ import annotations import os -from typing import Any, Union, Mapping -from typing_extensions import Self, override +from typing import Any, Dict, Union, Mapping, cast +from typing_extensions import Self, Literal, override import httpx @@ -30,7 +30,22 @@ AsyncAPIClient, ) -__all__ = ["Timeout", "Transport", "ProxiesTypes", "RequestOptions", "Kernel", "AsyncKernel", "Client", "AsyncClient"] +__all__ = [ + "ENVIRONMENTS", + "Timeout", + "Transport", + "ProxiesTypes", + "RequestOptions", + "Kernel", + "AsyncKernel", + "Client", + "AsyncClient", +] + +ENVIRONMENTS: Dict[str, str] = { + "production": "https://api.onkernel.com/", + "development": "https://localhost:3001/", +} class Kernel(SyncAPIClient): @@ -42,11 +57,14 @@ class Kernel(SyncAPIClient): # client options api_key: str + _environment: Literal["production", "development"] | NotGiven + def __init__( self, *, api_key: str | None = None, - base_url: str | httpx.URL | None = None, + environment: Literal["production", "development"] | NotGiven = NOT_GIVEN, + base_url: str | httpx.URL | None | NotGiven = NOT_GIVEN, timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, @@ -77,10 +95,31 @@ def __init__( ) self.api_key = api_key - if base_url is None: - base_url = os.environ.get("KERNEL_BASE_URL") - if base_url is None: - base_url = f"http://localhost:3001" + self._environment = environment + + base_url_env = os.environ.get("KERNEL_BASE_URL") + if is_given(base_url) and base_url is not None: + # cast required because mypy doesn't understand the type narrowing + base_url = cast("str | httpx.URL", base_url) # pyright: ignore[reportUnnecessaryCast] + elif is_given(environment): + if base_url_env and base_url is not None: + raise ValueError( + "Ambiguous URL; The `KERNEL_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None", + ) + + try: + base_url = ENVIRONMENTS[environment] + except KeyError as exc: + raise ValueError(f"Unknown environment: {environment}") from exc + elif base_url_env is not None: + base_url = base_url_env + else: + self._environment = environment = "production" + + try: + base_url = ENVIRONMENTS[environment] + except KeyError as exc: + raise ValueError(f"Unknown environment: {environment}") from exc super().__init__( version=__version__, @@ -122,6 +161,7 @@ def copy( self, *, api_key: str | None = None, + environment: Literal["production", "development"] | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = NOT_GIVEN, http_client: httpx.Client | None = None, @@ -157,6 +197,7 @@ def copy( return self.__class__( api_key=api_key or self.api_key, base_url=base_url or self.base_url, + environment=environment or self._environment, timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, http_client=http_client, max_retries=max_retries if is_given(max_retries) else self.max_retries, @@ -212,11 +253,14 @@ class AsyncKernel(AsyncAPIClient): # client options api_key: str + _environment: Literal["production", "development"] | NotGiven + def __init__( self, *, api_key: str | None = None, - base_url: str | httpx.URL | None = None, + environment: Literal["production", "development"] | NotGiven = NOT_GIVEN, + base_url: str | httpx.URL | None | NotGiven = NOT_GIVEN, timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, @@ -247,10 +291,31 @@ def __init__( ) self.api_key = api_key - if base_url is None: - base_url = os.environ.get("KERNEL_BASE_URL") - if base_url is None: - base_url = f"http://localhost:3001" + self._environment = environment + + base_url_env = os.environ.get("KERNEL_BASE_URL") + if is_given(base_url) and base_url is not None: + # cast required because mypy doesn't understand the type narrowing + base_url = cast("str | httpx.URL", base_url) # pyright: ignore[reportUnnecessaryCast] + elif is_given(environment): + if base_url_env and base_url is not None: + raise ValueError( + "Ambiguous URL; The `KERNEL_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None", + ) + + try: + base_url = ENVIRONMENTS[environment] + except KeyError as exc: + raise ValueError(f"Unknown environment: {environment}") from exc + elif base_url_env is not None: + base_url = base_url_env + else: + self._environment = environment = "production" + + try: + base_url = ENVIRONMENTS[environment] + except KeyError as exc: + raise ValueError(f"Unknown environment: {environment}") from exc super().__init__( version=__version__, @@ -292,6 +357,7 @@ def copy( self, *, api_key: str | None = None, + environment: Literal["production", "development"] | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = NOT_GIVEN, http_client: httpx.AsyncClient | None = None, @@ -327,6 +393,7 @@ def copy( return self.__class__( api_key=api_key or self.api_key, base_url=base_url or self.base_url, + environment=environment or self._environment, timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, http_client=http_client, max_retries=max_retries if is_given(max_retries) else self.max_retries, diff --git a/tests/test_client.py b/tests/test_client.py index f989403e..0efa1c24 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -553,6 +553,14 @@ def test_base_url_env(self) -> None: client = Kernel(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" + # explicit environment arg requires explicitness + with update_env(KERNEL_BASE_URL="http://localhost:5000/from/env"): + with pytest.raises(ValueError, match=r"you must pass base_url=None"): + Kernel(api_key=api_key, _strict_response_validation=True, environment="production") + + client = Kernel(base_url=None, api_key=api_key, _strict_response_validation=True, environment="production") + assert str(client.base_url).startswith("https://api.onkernel.com/") + @pytest.mark.parametrize( "client", [ @@ -1339,6 +1347,16 @@ def test_base_url_env(self) -> None: client = AsyncKernel(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" + # explicit environment arg requires explicitness + with update_env(KERNEL_BASE_URL="http://localhost:5000/from/env"): + with pytest.raises(ValueError, match=r"you must pass base_url=None"): + AsyncKernel(api_key=api_key, _strict_response_validation=True, environment="production") + + client = AsyncKernel( + base_url=None, api_key=api_key, _strict_response_validation=True, environment="production" + ) + assert str(client.base_url).startswith("https://api.onkernel.com/") + @pytest.mark.parametrize( "client", [ From 0969a06ea992184d94060bcd23d9174a63abcd32 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 19:24:53 +0000 Subject: [PATCH 017/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index b56c3d0b..e8285b71 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.4" + ".": "0.1.0-alpha.5" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 8764ccde..1bca88c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.1.0-alpha.4" +version = "0.1.0-alpha.5" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index f5e84303..55b53c6d 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.1.0-alpha.4" # x-release-please-version +__version__ = "0.1.0-alpha.5" # x-release-please-version From 15371f2c96b882f214401e9e5184f42d7acbb047 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 11 May 2025 10:42:19 +0000 Subject: [PATCH 018/448] feat(api): update via SDK Studio --- .stats.yml | 4 +- api.md | 3 +- src/kernel/resources/apps.py | 79 ---------------- src/kernel/types/__init__.py | 1 - .../types/app_retrieve_invocation_response.py | 25 ------ tests/api_resources/test_apps.py | 90 +------------------ 6 files changed, 4 insertions(+), 198 deletions(-) delete mode 100644 src/kernel/types/app_retrieve_invocation_response.py diff --git a/.stats.yml b/.stats.yml index 1a7891f2..42510a99 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 4 +configured_endpoints: 3 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-d168b58fcf39dbd0458d132091793d3e2d0930070b7dda2d5f7f1baff20dd31b.yml openapi_spec_hash: b7e0fd7ee1656d7dbad57209d1584d92 -config_hash: 2d282609080a6011e3f6222451f72237 +config_hash: 9139d1eb064baf60fd2265aac382f097 diff --git a/api.md b/api.md index ec9b481c..fe6fb484 100644 --- a/api.md +++ b/api.md @@ -3,14 +3,13 @@ Types: ```python -from kernel.types import AppDeployResponse, AppInvokeResponse, AppRetrieveInvocationResponse +from kernel.types import AppDeployResponse, AppInvokeResponse ``` Methods: - client.apps.deploy(\*\*params) -> AppDeployResponse - client.apps.invoke(\*\*params) -> AppInvokeResponse -- client.apps.retrieve_invocation(id) -> AppRetrieveInvocationResponse # Browser diff --git a/src/kernel/resources/apps.py b/src/kernel/resources/apps.py index 997a27f0..24cc1542 100644 --- a/src/kernel/resources/apps.py +++ b/src/kernel/resources/apps.py @@ -20,7 +20,6 @@ from .._base_client import make_request_options from ..types.app_deploy_response import AppDeployResponse from ..types.app_invoke_response import AppInvokeResponse -from ..types.app_retrieve_invocation_response import AppRetrieveInvocationResponse __all__ = ["AppsResource", "AsyncAppsResource"] @@ -153,39 +152,6 @@ def invoke( cast_to=AppInvokeResponse, ) - def retrieve_invocation( - self, - id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AppRetrieveInvocationResponse: - """ - Get an app invocation by id - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - return self._get( - f"/apps/invocations/{id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AppRetrieveInvocationResponse, - ) - class AsyncAppsResource(AsyncAPIResource): @cached_property @@ -315,39 +281,6 @@ async def invoke( cast_to=AppInvokeResponse, ) - async def retrieve_invocation( - self, - id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AppRetrieveInvocationResponse: - """ - Get an app invocation by id - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - return await self._get( - f"/apps/invocations/{id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AppRetrieveInvocationResponse, - ) - class AppsResourceWithRawResponse: def __init__(self, apps: AppsResource) -> None: @@ -359,9 +292,6 @@ def __init__(self, apps: AppsResource) -> None: self.invoke = to_raw_response_wrapper( apps.invoke, ) - self.retrieve_invocation = to_raw_response_wrapper( - apps.retrieve_invocation, - ) class AsyncAppsResourceWithRawResponse: @@ -374,9 +304,6 @@ def __init__(self, apps: AsyncAppsResource) -> None: self.invoke = async_to_raw_response_wrapper( apps.invoke, ) - self.retrieve_invocation = async_to_raw_response_wrapper( - apps.retrieve_invocation, - ) class AppsResourceWithStreamingResponse: @@ -389,9 +316,6 @@ def __init__(self, apps: AppsResource) -> None: self.invoke = to_streamed_response_wrapper( apps.invoke, ) - self.retrieve_invocation = to_streamed_response_wrapper( - apps.retrieve_invocation, - ) class AsyncAppsResourceWithStreamingResponse: @@ -404,6 +328,3 @@ def __init__(self, apps: AsyncAppsResource) -> None: self.invoke = async_to_streamed_response_wrapper( apps.invoke, ) - self.retrieve_invocation = async_to_streamed_response_wrapper( - apps.retrieve_invocation, - ) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 9577c2f8..2403d118 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -7,4 +7,3 @@ from .app_deploy_response import AppDeployResponse as AppDeployResponse from .app_invoke_response import AppInvokeResponse as AppInvokeResponse from .browser_create_session_response import BrowserCreateSessionResponse as BrowserCreateSessionResponse -from .app_retrieve_invocation_response import AppRetrieveInvocationResponse as AppRetrieveInvocationResponse diff --git a/src/kernel/types/app_retrieve_invocation_response.py b/src/kernel/types/app_retrieve_invocation_response.py deleted file mode 100644 index 8b3de1f8..00000000 --- a/src/kernel/types/app_retrieve_invocation_response.py +++ /dev/null @@ -1,25 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["AppRetrieveInvocationResponse"] - - -class AppRetrieveInvocationResponse(BaseModel): - id: str - - app_name: str = FieldInfo(alias="appName") - - finished_at: Optional[str] = FieldInfo(alias="finishedAt", default=None) - - input: str - - output: str - - started_at: str = FieldInfo(alias="startedAt") - - status: str diff --git a/tests/api_resources/test_apps.py b/tests/api_resources/test_apps.py index 26d0ef11..962d7aa2 100644 --- a/tests/api_resources/test_apps.py +++ b/tests/api_resources/test_apps.py @@ -9,11 +9,7 @@ from kernel import Kernel, AsyncKernel from tests.utils import assert_matches_type -from kernel.types import ( - AppDeployResponse, - AppInvokeResponse, - AppRetrieveInvocationResponse, -) +from kernel.types import AppDeployResponse, AppInvokeResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -115,48 +111,6 @@ def test_streaming_response_invoke(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip() - @parametrize - def test_method_retrieve_invocation(self, client: Kernel) -> None: - app = client.apps.retrieve_invocation( - "id", - ) - assert_matches_type(AppRetrieveInvocationResponse, app, path=["response"]) - - @pytest.mark.skip() - @parametrize - def test_raw_response_retrieve_invocation(self, client: Kernel) -> None: - response = client.apps.with_raw_response.retrieve_invocation( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - app = response.parse() - assert_matches_type(AppRetrieveInvocationResponse, app, path=["response"]) - - @pytest.mark.skip() - @parametrize - def test_streaming_response_retrieve_invocation(self, client: Kernel) -> None: - with client.apps.with_streaming_response.retrieve_invocation( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - app = response.parse() - assert_matches_type(AppRetrieveInvocationResponse, app, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip() - @parametrize - def test_path_params_retrieve_invocation(self, client: Kernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - client.apps.with_raw_response.retrieve_invocation( - "", - ) - class TestAsyncApps: parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) @@ -254,45 +208,3 @@ async def test_streaming_response_invoke(self, async_client: AsyncKernel) -> Non assert_matches_type(AppInvokeResponse, app, path=["response"]) assert cast(Any, response.is_closed) is True - - @pytest.mark.skip() - @parametrize - async def test_method_retrieve_invocation(self, async_client: AsyncKernel) -> None: - app = await async_client.apps.retrieve_invocation( - "id", - ) - assert_matches_type(AppRetrieveInvocationResponse, app, path=["response"]) - - @pytest.mark.skip() - @parametrize - async def test_raw_response_retrieve_invocation(self, async_client: AsyncKernel) -> None: - response = await async_client.apps.with_raw_response.retrieve_invocation( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - app = await response.parse() - assert_matches_type(AppRetrieveInvocationResponse, app, path=["response"]) - - @pytest.mark.skip() - @parametrize - async def test_streaming_response_retrieve_invocation(self, async_client: AsyncKernel) -> None: - async with async_client.apps.with_streaming_response.retrieve_invocation( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - app = await response.parse() - assert_matches_type(AppRetrieveInvocationResponse, app, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip() - @parametrize - async def test_path_params_retrieve_invocation(self, async_client: AsyncKernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - await async_client.apps.with_raw_response.retrieve_invocation( - "", - ) From 8825f9b9a56239566bced44a1222b441a74d1218 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 11 May 2025 10:45:52 +0000 Subject: [PATCH 019/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e8285b71..4f9005ea 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.5" + ".": "0.1.0-alpha.6" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 1bca88c2..a714f44f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.1.0-alpha.5" +version = "0.1.0-alpha.6" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 55b53c6d..2451c60b 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.1.0-alpha.5" # x-release-please-version +__version__ = "0.1.0-alpha.6" # x-release-please-version From f51c6b3fe72d0ccc38ee5939a8e6e064ec77dedd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 11 May 2025 11:26:30 +0000 Subject: [PATCH 020/448] feat(api): update via SDK Studio --- .stats.yml | 4 +- api.md | 3 +- src/kernel/resources/apps.py | 79 ++++++++++++++++ src/kernel/types/__init__.py | 1 + .../types/app_retrieve_invocation_response.py | 25 ++++++ tests/api_resources/test_apps.py | 90 ++++++++++++++++++- 6 files changed, 198 insertions(+), 4 deletions(-) create mode 100644 src/kernel/types/app_retrieve_invocation_response.py diff --git a/.stats.yml b/.stats.yml index 42510a99..6af3fb9c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 3 +configured_endpoints: 4 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-d168b58fcf39dbd0458d132091793d3e2d0930070b7dda2d5f7f1baff20dd31b.yml openapi_spec_hash: b7e0fd7ee1656d7dbad57209d1584d92 -config_hash: 9139d1eb064baf60fd2265aac382f097 +config_hash: eab40627b734534462ae3b8ccd8b263b diff --git a/api.md b/api.md index fe6fb484..ec9b481c 100644 --- a/api.md +++ b/api.md @@ -3,13 +3,14 @@ Types: ```python -from kernel.types import AppDeployResponse, AppInvokeResponse +from kernel.types import AppDeployResponse, AppInvokeResponse, AppRetrieveInvocationResponse ``` Methods: - client.apps.deploy(\*\*params) -> AppDeployResponse - client.apps.invoke(\*\*params) -> AppInvokeResponse +- client.apps.retrieve_invocation(id) -> AppRetrieveInvocationResponse # Browser diff --git a/src/kernel/resources/apps.py b/src/kernel/resources/apps.py index 24cc1542..997a27f0 100644 --- a/src/kernel/resources/apps.py +++ b/src/kernel/resources/apps.py @@ -20,6 +20,7 @@ from .._base_client import make_request_options from ..types.app_deploy_response import AppDeployResponse from ..types.app_invoke_response import AppInvokeResponse +from ..types.app_retrieve_invocation_response import AppRetrieveInvocationResponse __all__ = ["AppsResource", "AsyncAppsResource"] @@ -152,6 +153,39 @@ def invoke( cast_to=AppInvokeResponse, ) + def retrieve_invocation( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AppRetrieveInvocationResponse: + """ + Get an app invocation by id + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/apps/invocations/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AppRetrieveInvocationResponse, + ) + class AsyncAppsResource(AsyncAPIResource): @cached_property @@ -281,6 +315,39 @@ async def invoke( cast_to=AppInvokeResponse, ) + async def retrieve_invocation( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AppRetrieveInvocationResponse: + """ + Get an app invocation by id + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/apps/invocations/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AppRetrieveInvocationResponse, + ) + class AppsResourceWithRawResponse: def __init__(self, apps: AppsResource) -> None: @@ -292,6 +359,9 @@ def __init__(self, apps: AppsResource) -> None: self.invoke = to_raw_response_wrapper( apps.invoke, ) + self.retrieve_invocation = to_raw_response_wrapper( + apps.retrieve_invocation, + ) class AsyncAppsResourceWithRawResponse: @@ -304,6 +374,9 @@ def __init__(self, apps: AsyncAppsResource) -> None: self.invoke = async_to_raw_response_wrapper( apps.invoke, ) + self.retrieve_invocation = async_to_raw_response_wrapper( + apps.retrieve_invocation, + ) class AppsResourceWithStreamingResponse: @@ -316,6 +389,9 @@ def __init__(self, apps: AppsResource) -> None: self.invoke = to_streamed_response_wrapper( apps.invoke, ) + self.retrieve_invocation = to_streamed_response_wrapper( + apps.retrieve_invocation, + ) class AsyncAppsResourceWithStreamingResponse: @@ -328,3 +404,6 @@ def __init__(self, apps: AsyncAppsResource) -> None: self.invoke = async_to_streamed_response_wrapper( apps.invoke, ) + self.retrieve_invocation = async_to_streamed_response_wrapper( + apps.retrieve_invocation, + ) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 2403d118..9577c2f8 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -7,3 +7,4 @@ from .app_deploy_response import AppDeployResponse as AppDeployResponse from .app_invoke_response import AppInvokeResponse as AppInvokeResponse from .browser_create_session_response import BrowserCreateSessionResponse as BrowserCreateSessionResponse +from .app_retrieve_invocation_response import AppRetrieveInvocationResponse as AppRetrieveInvocationResponse diff --git a/src/kernel/types/app_retrieve_invocation_response.py b/src/kernel/types/app_retrieve_invocation_response.py new file mode 100644 index 00000000..8b3de1f8 --- /dev/null +++ b/src/kernel/types/app_retrieve_invocation_response.py @@ -0,0 +1,25 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["AppRetrieveInvocationResponse"] + + +class AppRetrieveInvocationResponse(BaseModel): + id: str + + app_name: str = FieldInfo(alias="appName") + + finished_at: Optional[str] = FieldInfo(alias="finishedAt", default=None) + + input: str + + output: str + + started_at: str = FieldInfo(alias="startedAt") + + status: str diff --git a/tests/api_resources/test_apps.py b/tests/api_resources/test_apps.py index 962d7aa2..26d0ef11 100644 --- a/tests/api_resources/test_apps.py +++ b/tests/api_resources/test_apps.py @@ -9,7 +9,11 @@ from kernel import Kernel, AsyncKernel from tests.utils import assert_matches_type -from kernel.types import AppDeployResponse, AppInvokeResponse +from kernel.types import ( + AppDeployResponse, + AppInvokeResponse, + AppRetrieveInvocationResponse, +) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -111,6 +115,48 @@ def test_streaming_response_invoke(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True + @pytest.mark.skip() + @parametrize + def test_method_retrieve_invocation(self, client: Kernel) -> None: + app = client.apps.retrieve_invocation( + "id", + ) + assert_matches_type(AppRetrieveInvocationResponse, app, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_raw_response_retrieve_invocation(self, client: Kernel) -> None: + response = client.apps.with_raw_response.retrieve_invocation( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + app = response.parse() + assert_matches_type(AppRetrieveInvocationResponse, app, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_streaming_response_retrieve_invocation(self, client: Kernel) -> None: + with client.apps.with_streaming_response.retrieve_invocation( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + app = response.parse() + assert_matches_type(AppRetrieveInvocationResponse, app, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + def test_path_params_retrieve_invocation(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.apps.with_raw_response.retrieve_invocation( + "", + ) + class TestAsyncApps: parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) @@ -208,3 +254,45 @@ async def test_streaming_response_invoke(self, async_client: AsyncKernel) -> Non assert_matches_type(AppInvokeResponse, app, path=["response"]) assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + async def test_method_retrieve_invocation(self, async_client: AsyncKernel) -> None: + app = await async_client.apps.retrieve_invocation( + "id", + ) + assert_matches_type(AppRetrieveInvocationResponse, app, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_raw_response_retrieve_invocation(self, async_client: AsyncKernel) -> None: + response = await async_client.apps.with_raw_response.retrieve_invocation( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + app = await response.parse() + assert_matches_type(AppRetrieveInvocationResponse, app, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_retrieve_invocation(self, async_client: AsyncKernel) -> None: + async with async_client.apps.with_streaming_response.retrieve_invocation( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + app = await response.parse() + assert_matches_type(AppRetrieveInvocationResponse, app, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + async def test_path_params_retrieve_invocation(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.apps.with_raw_response.retrieve_invocation( + "", + ) From 83ca9a5a396902d6d2b39b1e2ec8598f13decee4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 11 May 2025 11:29:41 +0000 Subject: [PATCH 021/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 4f9005ea..b5db7ce1 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.6" + ".": "0.1.0-alpha.7" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index a714f44f..710383cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.1.0-alpha.6" +version = "0.1.0-alpha.7" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 2451c60b..76a72556 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.1.0-alpha.6" # x-release-please-version +__version__ = "0.1.0-alpha.7" # x-release-please-version From c429189ab661e9af1fec55faed837286257ee505 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 18:48:36 +0000 Subject: [PATCH 022/448] feat(api): update via SDK Studio --- .stats.yml | 4 +- README.md | 15 +---- api.md | 2 +- src/kernel/resources/apps.py | 45 +++++++++------ src/kernel/resources/browser.py | 40 ++++++++++++- src/kernel/types/__init__.py | 1 + src/kernel/types/app_deploy_params.py | 21 ++++--- src/kernel/types/app_deploy_response.py | 22 +++++++- .../types/browser_create_session_params.py | 14 +++++ tests/api_resources/test_apps.py | 34 ++++------- tests/api_resources/test_browser.py | 24 ++++++-- tests/test_client.py | 56 ++++--------------- 12 files changed, 157 insertions(+), 121 deletions(-) create mode 100644 src/kernel/types/browser_create_session_params.py diff --git a/.stats.yml b/.stats.yml index 6af3fb9c..cbc2ff8f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 4 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-d168b58fcf39dbd0458d132091793d3e2d0930070b7dda2d5f7f1baff20dd31b.yml -openapi_spec_hash: b7e0fd7ee1656d7dbad57209d1584d92 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2af763aab4c314b382e1123edc4ee3d51c0fe7977730ce6776b9fb09b29fe291.yml +openapi_spec_hash: be02256478be81fa3f649076879850bc config_hash: eab40627b734534462ae3b8ccd8b263b diff --git a/README.md b/README.md index ab4e7655..1cbfb1f2 100644 --- a/README.md +++ b/README.md @@ -34,11 +34,10 @@ client = Kernel( ) response = client.apps.deploy( - app_name="REPLACE_ME", file=b"REPLACE_ME", version="REPLACE_ME", ) -print(response.id) +print(response.apps) ``` While you can provide an `api_key` keyword argument, @@ -64,11 +63,10 @@ client = AsyncKernel( async def main() -> None: response = await client.apps.deploy( - app_name="REPLACE_ME", file=b"REPLACE_ME", version="REPLACE_ME", ) - print(response.id) + print(response.apps) asyncio.run(main()) @@ -96,9 +94,7 @@ from kernel import Kernel client = Kernel() client.apps.deploy( - app_name="my-awesome-app", file=Path("/path/to/file"), - version="1.0.0", ) ``` @@ -121,7 +117,6 @@ client = Kernel() try: client.apps.deploy( - app_name="REPLACE_ME", file=b"REPLACE_ME", version="REPLACE_ME", ) @@ -168,7 +163,6 @@ client = Kernel( # Or, configure per-request: client.with_options(max_retries=5).apps.deploy( - app_name="REPLACE_ME", file=b"REPLACE_ME", version="REPLACE_ME", ) @@ -195,7 +189,6 @@ client = Kernel( # Override per-request: client.with_options(timeout=5.0).apps.deploy( - app_name="REPLACE_ME", file=b"REPLACE_ME", version="REPLACE_ME", ) @@ -240,14 +233,13 @@ from kernel import Kernel client = Kernel() response = client.apps.with_raw_response.deploy( - app_name="REPLACE_ME", file=b"REPLACE_ME", version="REPLACE_ME", ) print(response.headers.get('X-My-Header')) app = response.parse() # get the object that `apps.deploy()` would have returned -print(app.id) +print(app.apps) ``` These methods return an [`APIResponse`](https://github.com/onkernel/kernel-python-sdk/tree/main/src/kernel/_response.py) object. @@ -262,7 +254,6 @@ To stream the response body, use `.with_streaming_response` instead, which requi ```python with client.apps.with_streaming_response.deploy( - app_name="REPLACE_ME", file=b"REPLACE_ME", version="REPLACE_ME", ) as response: diff --git a/api.md b/api.md index ec9b481c..581c76f6 100644 --- a/api.md +++ b/api.md @@ -22,4 +22,4 @@ from kernel.types import BrowserCreateSessionResponse Methods: -- client.browser.create_session() -> BrowserCreateSessionResponse +- client.browser.create_session(\*\*params) -> BrowserCreateSessionResponse diff --git a/src/kernel/resources/apps.py b/src/kernel/resources/apps.py index 997a27f0..45148805 100644 --- a/src/kernel/resources/apps.py +++ b/src/kernel/resources/apps.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Mapping, cast +from typing_extensions import Literal import httpx @@ -48,10 +49,11 @@ def with_streaming_response(self) -> AppsResourceWithStreamingResponse: def deploy( self, *, - app_name: str, file: FileTypes, - version: str, - region: str | NotGiven = NOT_GIVEN, + entrypoint_rel_path: str | NotGiven = NOT_GIVEN, + force: Literal["true", "false"] | NotGiven = NOT_GIVEN, + region: Literal["aws.us-east-1a"] | NotGiven = NOT_GIVEN, + version: 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, @@ -63,13 +65,15 @@ def deploy( Deploy a new application Args: - app_name: Name of the application + file: ZIP file containing the application source directory - file: ZIP file containing the application + entrypoint_rel_path: Relative path to the entrypoint of the application - version: Version of the application + force: Allow overwriting an existing app version - region: AWS region for deployment (e.g. "aws.us-east-1a") + region: Region for deployment. Currently we only support "aws.us-east-1a" + + version: Version of the application. Can be any string. extra_headers: Send extra headers @@ -81,10 +85,11 @@ def deploy( """ body = deepcopy_minimal( { - "app_name": app_name, "file": file, - "version": version, + "entrypoint_rel_path": entrypoint_rel_path, + "force": force, "region": region, + "version": version, } ) files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) @@ -210,10 +215,11 @@ def with_streaming_response(self) -> AsyncAppsResourceWithStreamingResponse: async def deploy( self, *, - app_name: str, file: FileTypes, - version: str, - region: str | NotGiven = NOT_GIVEN, + entrypoint_rel_path: str | NotGiven = NOT_GIVEN, + force: Literal["true", "false"] | NotGiven = NOT_GIVEN, + region: Literal["aws.us-east-1a"] | NotGiven = NOT_GIVEN, + version: 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, @@ -225,13 +231,15 @@ async def deploy( Deploy a new application Args: - app_name: Name of the application + file: ZIP file containing the application source directory - file: ZIP file containing the application + entrypoint_rel_path: Relative path to the entrypoint of the application - version: Version of the application + force: Allow overwriting an existing app version - region: AWS region for deployment (e.g. "aws.us-east-1a") + region: Region for deployment. Currently we only support "aws.us-east-1a" + + version: Version of the application. Can be any string. extra_headers: Send extra headers @@ -243,10 +251,11 @@ async def deploy( """ body = deepcopy_minimal( { - "app_name": app_name, "file": file, - "version": version, + "entrypoint_rel_path": entrypoint_rel_path, + "force": force, "region": region, + "version": version, } ) files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) diff --git a/src/kernel/resources/browser.py b/src/kernel/resources/browser.py index 3e3da668..3edf8c08 100644 --- a/src/kernel/resources/browser.py +++ b/src/kernel/resources/browser.py @@ -4,7 +4,9 @@ import httpx +from ..types import browser_create_session_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 ( @@ -42,6 +44,7 @@ def with_streaming_response(self) -> BrowserResourceWithStreamingResponse: def create_session( self, *, + invocation_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, @@ -49,9 +52,25 @@ def create_session( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> BrowserCreateSessionResponse: - """Create Browser Session""" + """ + Create Browser Session + + Args: + invocation_id: Kernel App invocation ID + + 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", + body=maybe_transform( + {"invocation_id": invocation_id}, browser_create_session_params.BrowserCreateSessionParams + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -82,6 +101,7 @@ def with_streaming_response(self) -> AsyncBrowserResourceWithStreamingResponse: async def create_session( self, *, + invocation_id: str, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -89,9 +109,25 @@ async def create_session( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> BrowserCreateSessionResponse: - """Create Browser Session""" + """ + Create Browser Session + + Args: + invocation_id: Kernel App invocation ID + + 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", + body=await async_maybe_transform( + {"invocation_id": invocation_id}, browser_create_session_params.BrowserCreateSessionParams + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 9577c2f8..32a47687 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -6,5 +6,6 @@ from .app_invoke_params import AppInvokeParams as AppInvokeParams from .app_deploy_response import AppDeployResponse as AppDeployResponse from .app_invoke_response import AppInvokeResponse as AppInvokeResponse +from .browser_create_session_params import BrowserCreateSessionParams as BrowserCreateSessionParams from .browser_create_session_response import BrowserCreateSessionResponse as BrowserCreateSessionResponse from .app_retrieve_invocation_response import AppRetrieveInvocationResponse as AppRetrieveInvocationResponse diff --git a/src/kernel/types/app_deploy_params.py b/src/kernel/types/app_deploy_params.py index 78a11f26..ff7242cb 100644 --- a/src/kernel/types/app_deploy_params.py +++ b/src/kernel/types/app_deploy_params.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing_extensions import Required, Annotated, TypedDict +from typing_extensions import Literal, Required, Annotated, TypedDict from .._types import FileTypes from .._utils import PropertyInfo @@ -11,14 +11,17 @@ class AppDeployParams(TypedDict, total=False): - app_name: Required[Annotated[str, PropertyInfo(alias="appName")]] - """Name of the application""" - file: Required[FileTypes] - """ZIP file containing the application""" + """ZIP file containing the application source directory""" + + entrypoint_rel_path: Annotated[str, PropertyInfo(alias="entrypointRelPath")] + """Relative path to the entrypoint of the application""" + + force: Literal["true", "false"] + """Allow overwriting an existing app version""" - version: Required[str] - """Version of the application""" + region: Literal["aws.us-east-1a"] + """Region for deployment. Currently we only support "aws.us-east-1a" """ - region: str - """AWS region for deployment (e.g. "aws.us-east-1a")""" + version: str + """Version of the application. Can be any string.""" diff --git a/src/kernel/types/app_deploy_response.py b/src/kernel/types/app_deploy_response.py index 6a214df6..e82164ea 100644 --- a/src/kernel/types/app_deploy_response.py +++ b/src/kernel/types/app_deploy_response.py @@ -1,13 +1,29 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from typing import List + from .._models import BaseModel -__all__ = ["AppDeployResponse"] +__all__ = ["AppDeployResponse", "App", "AppAction"] -class AppDeployResponse(BaseModel): +class AppAction(BaseModel): + name: str + """Name of the action""" + + +class App(BaseModel): id: str - """ID of the deployed app version""" + """ID for the app version deployed""" + + actions: List[AppAction] + + name: str + """Name of the app""" + + +class AppDeployResponse(BaseModel): + apps: List[App] message: str """Success message""" diff --git a/src/kernel/types/browser_create_session_params.py b/src/kernel/types/browser_create_session_params.py new file mode 100644 index 00000000..73389bee --- /dev/null +++ b/src/kernel/types/browser_create_session_params.py @@ -0,0 +1,14 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, Annotated, TypedDict + +from .._utils import PropertyInfo + +__all__ = ["BrowserCreateSessionParams"] + + +class BrowserCreateSessionParams(TypedDict, total=False): + invocation_id: Required[Annotated[str, PropertyInfo(alias="invocationId")]] + """Kernel App invocation ID""" diff --git a/tests/api_resources/test_apps.py b/tests/api_resources/test_apps.py index 26d0ef11..08efe17b 100644 --- a/tests/api_resources/test_apps.py +++ b/tests/api_resources/test_apps.py @@ -25,9 +25,7 @@ class TestApps: @parametrize def test_method_deploy(self, client: Kernel) -> None: app = client.apps.deploy( - app_name="my-awesome-app", file=b"raw file contents", - version="1.0.0", ) assert_matches_type(AppDeployResponse, app, path=["response"]) @@ -35,10 +33,11 @@ def test_method_deploy(self, client: Kernel) -> None: @parametrize def test_method_deploy_with_all_params(self, client: Kernel) -> None: app = client.apps.deploy( - app_name="my-awesome-app", file=b"raw file contents", - version="1.0.0", + entrypoint_rel_path="app.py", + force="false", region="aws.us-east-1a", + version="1.0.0", ) assert_matches_type(AppDeployResponse, app, path=["response"]) @@ -46,9 +45,7 @@ def test_method_deploy_with_all_params(self, client: Kernel) -> None: @parametrize def test_raw_response_deploy(self, client: Kernel) -> None: response = client.apps.with_raw_response.deploy( - app_name="my-awesome-app", file=b"raw file contents", - version="1.0.0", ) assert response.is_closed is True @@ -60,9 +57,7 @@ def test_raw_response_deploy(self, client: Kernel) -> None: @parametrize def test_streaming_response_deploy(self, client: Kernel) -> None: with client.apps.with_streaming_response.deploy( - app_name="my-awesome-app", file=b"raw file contents", - version="1.0.0", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -78,7 +73,7 @@ def test_method_invoke(self, client: Kernel) -> None: app = client.apps.invoke( action_name="analyze", app_name="my-awesome-app", - payload='{ "data": "example input" }', + payload={"data": "example input"}, version="1.0.0", ) assert_matches_type(AppInvokeResponse, app, path=["response"]) @@ -89,7 +84,7 @@ def test_raw_response_invoke(self, client: Kernel) -> None: response = client.apps.with_raw_response.invoke( action_name="analyze", app_name="my-awesome-app", - payload='{ "data": "example input" }', + payload={"data": "example input"}, version="1.0.0", ) @@ -104,7 +99,7 @@ def test_streaming_response_invoke(self, client: Kernel) -> None: with client.apps.with_streaming_response.invoke( action_name="analyze", app_name="my-awesome-app", - payload='{ "data": "example input" }', + payload={"data": "example input"}, version="1.0.0", ) as response: assert not response.is_closed @@ -165,9 +160,7 @@ class TestAsyncApps: @parametrize async def test_method_deploy(self, async_client: AsyncKernel) -> None: app = await async_client.apps.deploy( - app_name="my-awesome-app", file=b"raw file contents", - version="1.0.0", ) assert_matches_type(AppDeployResponse, app, path=["response"]) @@ -175,10 +168,11 @@ async def test_method_deploy(self, async_client: AsyncKernel) -> None: @parametrize async def test_method_deploy_with_all_params(self, async_client: AsyncKernel) -> None: app = await async_client.apps.deploy( - app_name="my-awesome-app", file=b"raw file contents", - version="1.0.0", + entrypoint_rel_path="app.py", + force="false", region="aws.us-east-1a", + version="1.0.0", ) assert_matches_type(AppDeployResponse, app, path=["response"]) @@ -186,9 +180,7 @@ async def test_method_deploy_with_all_params(self, async_client: AsyncKernel) -> @parametrize async def test_raw_response_deploy(self, async_client: AsyncKernel) -> None: response = await async_client.apps.with_raw_response.deploy( - app_name="my-awesome-app", file=b"raw file contents", - version="1.0.0", ) assert response.is_closed is True @@ -200,9 +192,7 @@ async def test_raw_response_deploy(self, async_client: AsyncKernel) -> None: @parametrize async def test_streaming_response_deploy(self, async_client: AsyncKernel) -> None: async with async_client.apps.with_streaming_response.deploy( - app_name="my-awesome-app", file=b"raw file contents", - version="1.0.0", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -218,7 +208,7 @@ async def test_method_invoke(self, async_client: AsyncKernel) -> None: app = await async_client.apps.invoke( action_name="analyze", app_name="my-awesome-app", - payload='{ "data": "example input" }', + payload={"data": "example input"}, version="1.0.0", ) assert_matches_type(AppInvokeResponse, app, path=["response"]) @@ -229,7 +219,7 @@ async def test_raw_response_invoke(self, async_client: AsyncKernel) -> None: response = await async_client.apps.with_raw_response.invoke( action_name="analyze", app_name="my-awesome-app", - payload='{ "data": "example input" }', + payload={"data": "example input"}, version="1.0.0", ) @@ -244,7 +234,7 @@ async def test_streaming_response_invoke(self, async_client: AsyncKernel) -> Non async with async_client.apps.with_streaming_response.invoke( action_name="analyze", app_name="my-awesome-app", - payload='{ "data": "example input" }', + payload={"data": "example input"}, version="1.0.0", ) as response: assert not response.is_closed diff --git a/tests/api_resources/test_browser.py b/tests/api_resources/test_browser.py index 1aa4a1c8..3280e05b 100644 --- a/tests/api_resources/test_browser.py +++ b/tests/api_resources/test_browser.py @@ -20,13 +20,17 @@ class TestBrowser: @pytest.mark.skip() @parametrize def test_method_create_session(self, client: Kernel) -> None: - browser = client.browser.create_session() + browser = client.browser.create_session( + invocation_id="invocationId", + ) assert_matches_type(BrowserCreateSessionResponse, browser, path=["response"]) @pytest.mark.skip() @parametrize def test_raw_response_create_session(self, client: Kernel) -> None: - response = client.browser.with_raw_response.create_session() + response = client.browser.with_raw_response.create_session( + invocation_id="invocationId", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -36,7 +40,9 @@ def test_raw_response_create_session(self, client: Kernel) -> None: @pytest.mark.skip() @parametrize def test_streaming_response_create_session(self, client: Kernel) -> None: - with client.browser.with_streaming_response.create_session() as response: + with client.browser.with_streaming_response.create_session( + invocation_id="invocationId", + ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -52,13 +58,17 @@ class TestAsyncBrowser: @pytest.mark.skip() @parametrize async def test_method_create_session(self, async_client: AsyncKernel) -> None: - browser = await async_client.browser.create_session() + browser = await async_client.browser.create_session( + invocation_id="invocationId", + ) assert_matches_type(BrowserCreateSessionResponse, browser, path=["response"]) @pytest.mark.skip() @parametrize async def test_raw_response_create_session(self, async_client: AsyncKernel) -> None: - response = await async_client.browser.with_raw_response.create_session() + response = await async_client.browser.with_raw_response.create_session( + invocation_id="invocationId", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -68,7 +78,9 @@ async def test_raw_response_create_session(self, async_client: AsyncKernel) -> N @pytest.mark.skip() @parametrize async def test_streaming_response_create_session(self, async_client: AsyncKernel) -> None: - async with async_client.browser.with_streaming_response.create_session() as response: + async with async_client.browser.with_streaming_response.create_session( + invocation_id="invocationId", + ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/test_client.py b/tests/test_client.py index 0efa1c24..360b6228 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -720,12 +720,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No with pytest.raises(APITimeoutError): self.client.post( "/apps/deploy", - body=cast( - object, - maybe_transform( - dict(app_name="REPLACE_ME", file=b"REPLACE_ME", version="REPLACE_ME"), AppDeployParams - ), - ), + body=cast(object, maybe_transform(dict(file=b"REPLACE_ME", version="REPLACE_ME"), AppDeployParams)), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -740,12 +735,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non with pytest.raises(APIStatusError): self.client.post( "/apps/deploy", - body=cast( - object, - maybe_transform( - dict(app_name="REPLACE_ME", file=b"REPLACE_ME", version="REPLACE_ME"), AppDeployParams - ), - ), + body=cast(object, maybe_transform(dict(file=b"REPLACE_ME", version="REPLACE_ME"), AppDeployParams)), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -778,9 +768,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/apps/deploy").mock(side_effect=retry_handler) - response = client.apps.with_raw_response.deploy( - app_name="my-awesome-app", file=b"raw file contents", version="1.0.0" - ) + response = client.apps.with_raw_response.deploy(file=b"raw file contents") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -805,10 +793,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/apps/deploy").mock(side_effect=retry_handler) response = client.apps.with_raw_response.deploy( - app_name="my-awesome-app", - file=b"raw file contents", - version="1.0.0", - extra_headers={"x-stainless-retry-count": Omit()}, + file=b"raw file contents", extra_headers={"x-stainless-retry-count": Omit()} ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -833,10 +818,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/apps/deploy").mock(side_effect=retry_handler) response = client.apps.with_raw_response.deploy( - app_name="my-awesome-app", - file=b"raw file contents", - version="1.0.0", - extra_headers={"x-stainless-retry-count": "42"}, + file=b"raw file contents", extra_headers={"x-stainless-retry-count": "42"} ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1528,12 +1510,7 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) with pytest.raises(APITimeoutError): await self.client.post( "/apps/deploy", - body=cast( - object, - maybe_transform( - dict(app_name="REPLACE_ME", file=b"REPLACE_ME", version="REPLACE_ME"), AppDeployParams - ), - ), + body=cast(object, maybe_transform(dict(file=b"REPLACE_ME", version="REPLACE_ME"), AppDeployParams)), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -1548,12 +1525,7 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) with pytest.raises(APIStatusError): await self.client.post( "/apps/deploy", - body=cast( - object, - maybe_transform( - dict(app_name="REPLACE_ME", file=b"REPLACE_ME", version="REPLACE_ME"), AppDeployParams - ), - ), + body=cast(object, maybe_transform(dict(file=b"REPLACE_ME", version="REPLACE_ME"), AppDeployParams)), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -1587,9 +1559,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/apps/deploy").mock(side_effect=retry_handler) - response = await client.apps.with_raw_response.deploy( - app_name="my-awesome-app", file=b"raw file contents", version="1.0.0" - ) + response = await client.apps.with_raw_response.deploy(file=b"raw file contents") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1615,10 +1585,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/apps/deploy").mock(side_effect=retry_handler) response = await client.apps.with_raw_response.deploy( - app_name="my-awesome-app", - file=b"raw file contents", - version="1.0.0", - extra_headers={"x-stainless-retry-count": Omit()}, + file=b"raw file contents", extra_headers={"x-stainless-retry-count": Omit()} ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -1644,10 +1611,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/apps/deploy").mock(side_effect=retry_handler) response = await client.apps.with_raw_response.deploy( - app_name="my-awesome-app", - file=b"raw file contents", - version="1.0.0", - extra_headers={"x-stainless-retry-count": "42"}, + file=b"raw file contents", extra_headers={"x-stainless-retry-count": "42"} ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" From e54f65e11c75bdbe660bb86ee5e1736ae909983a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 18:50:55 +0000 Subject: [PATCH 023/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index b5db7ce1..c373724d 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.7" + ".": "0.1.0-alpha.8" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 710383cd..37fb0034 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.1.0-alpha.7" +version = "0.1.0-alpha.8" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 76a72556..924d7142 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.1.0-alpha.7" # x-release-please-version +__version__ = "0.1.0-alpha.8" # x-release-please-version From b266b3aabacd8a837c7132d711b7789f04bb5ccb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 22:51:22 +0000 Subject: [PATCH 024/448] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index cbc2ff8f..1b796feb 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 4 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2af763aab4c314b382e1123edc4ee3d51c0fe7977730ce6776b9fb09b29fe291.yml openapi_spec_hash: be02256478be81fa3f649076879850bc -config_hash: eab40627b734534462ae3b8ccd8b263b +config_hash: 71cb25ebb05ff0dd0e98c3b2ee091bc4 From efa4c306744b12cb7452ede596fbb998a2ea67f8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 22:52:45 +0000 Subject: [PATCH 025/448] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 1b796feb..fbd02611 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 4 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2af763aab4c314b382e1123edc4ee3d51c0fe7977730ce6776b9fb09b29fe291.yml openapi_spec_hash: be02256478be81fa3f649076879850bc -config_hash: 71cb25ebb05ff0dd0e98c3b2ee091bc4 +config_hash: 2c8351ba6611ce4a352e248405783846 From 952c1059203a963a69ccf60bce5199f34e72b4ab Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 22:53:33 +0000 Subject: [PATCH 026/448] feat(api): update via SDK Studio --- .stats.yml | 4 +-- README.md | 8 ++++++ src/kernel/resources/apps.py | 16 +++++------ src/kernel/types/app_deploy_params.py | 6 ++-- tests/api_resources/test_apps.py | 10 +++++-- tests/test_client.py | 40 ++++++++++++++++++++------- 6 files changed, 59 insertions(+), 25 deletions(-) diff --git a/.stats.yml b/.stats.yml index fbd02611..d6d797ad 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 4 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2af763aab4c314b382e1123edc4ee3d51c0fe7977730ce6776b9fb09b29fe291.yml -openapi_spec_hash: be02256478be81fa3f649076879850bc +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-07d481d1498bf9677437b555e9ec2d843d50107faa7501e4c430a32b1f3c3343.yml +openapi_spec_hash: 296f78d82afbac95fad12c5eabd71f18 config_hash: 2c8351ba6611ce4a352e248405783846 diff --git a/README.md b/README.md index 1cbfb1f2..801b34f8 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ client = Kernel( ) response = client.apps.deploy( + entrypoint_rel_path="app.py", file=b"REPLACE_ME", version="REPLACE_ME", ) @@ -63,6 +64,7 @@ client = AsyncKernel( async def main() -> None: response = await client.apps.deploy( + entrypoint_rel_path="app.py", file=b"REPLACE_ME", version="REPLACE_ME", ) @@ -94,6 +96,7 @@ from kernel import Kernel client = Kernel() client.apps.deploy( + entrypoint_rel_path="app.py", file=Path("/path/to/file"), ) ``` @@ -117,6 +120,7 @@ client = Kernel() try: client.apps.deploy( + entrypoint_rel_path="app.py", file=b"REPLACE_ME", version="REPLACE_ME", ) @@ -163,6 +167,7 @@ client = Kernel( # Or, configure per-request: client.with_options(max_retries=5).apps.deploy( + entrypoint_rel_path="app.py", file=b"REPLACE_ME", version="REPLACE_ME", ) @@ -189,6 +194,7 @@ client = Kernel( # Override per-request: client.with_options(timeout=5.0).apps.deploy( + entrypoint_rel_path="app.py", file=b"REPLACE_ME", version="REPLACE_ME", ) @@ -233,6 +239,7 @@ from kernel import Kernel client = Kernel() response = client.apps.with_raw_response.deploy( + entrypoint_rel_path="app.py", file=b"REPLACE_ME", version="REPLACE_ME", ) @@ -254,6 +261,7 @@ To stream the response body, use `.with_streaming_response` instead, which requi ```python with client.apps.with_streaming_response.deploy( + entrypoint_rel_path="app.py", file=b"REPLACE_ME", version="REPLACE_ME", ) as response: diff --git a/src/kernel/resources/apps.py b/src/kernel/resources/apps.py index 45148805..023f2143 100644 --- a/src/kernel/resources/apps.py +++ b/src/kernel/resources/apps.py @@ -49,8 +49,8 @@ def with_streaming_response(self) -> AppsResourceWithStreamingResponse: def deploy( self, *, + entrypoint_rel_path: str, file: FileTypes, - entrypoint_rel_path: str | NotGiven = NOT_GIVEN, force: Literal["true", "false"] | NotGiven = NOT_GIVEN, region: Literal["aws.us-east-1a"] | NotGiven = NOT_GIVEN, version: str | NotGiven = NOT_GIVEN, @@ -65,10 +65,10 @@ def deploy( Deploy a new application Args: - file: ZIP file containing the application source directory - entrypoint_rel_path: Relative path to the entrypoint of the application + file: ZIP file containing the application source directory + force: Allow overwriting an existing app version region: Region for deployment. Currently we only support "aws.us-east-1a" @@ -85,8 +85,8 @@ def deploy( """ body = deepcopy_minimal( { - "file": file, "entrypoint_rel_path": entrypoint_rel_path, + "file": file, "force": force, "region": region, "version": version, @@ -215,8 +215,8 @@ def with_streaming_response(self) -> AsyncAppsResourceWithStreamingResponse: async def deploy( self, *, + entrypoint_rel_path: str, file: FileTypes, - entrypoint_rel_path: str | NotGiven = NOT_GIVEN, force: Literal["true", "false"] | NotGiven = NOT_GIVEN, region: Literal["aws.us-east-1a"] | NotGiven = NOT_GIVEN, version: str | NotGiven = NOT_GIVEN, @@ -231,10 +231,10 @@ async def deploy( Deploy a new application Args: - file: ZIP file containing the application source directory - entrypoint_rel_path: Relative path to the entrypoint of the application + file: ZIP file containing the application source directory + force: Allow overwriting an existing app version region: Region for deployment. Currently we only support "aws.us-east-1a" @@ -251,8 +251,8 @@ async def deploy( """ body = deepcopy_minimal( { - "file": file, "entrypoint_rel_path": entrypoint_rel_path, + "file": file, "force": force, "region": region, "version": version, diff --git a/src/kernel/types/app_deploy_params.py b/src/kernel/types/app_deploy_params.py index ff7242cb..790743db 100644 --- a/src/kernel/types/app_deploy_params.py +++ b/src/kernel/types/app_deploy_params.py @@ -11,12 +11,12 @@ class AppDeployParams(TypedDict, total=False): + entrypoint_rel_path: Required[Annotated[str, PropertyInfo(alias="entrypointRelPath")]] + """Relative path to the entrypoint of the application""" + file: Required[FileTypes] """ZIP file containing the application source directory""" - entrypoint_rel_path: Annotated[str, PropertyInfo(alias="entrypointRelPath")] - """Relative path to the entrypoint of the application""" - force: Literal["true", "false"] """Allow overwriting an existing app version""" diff --git a/tests/api_resources/test_apps.py b/tests/api_resources/test_apps.py index 08efe17b..87194867 100644 --- a/tests/api_resources/test_apps.py +++ b/tests/api_resources/test_apps.py @@ -25,6 +25,7 @@ class TestApps: @parametrize def test_method_deploy(self, client: Kernel) -> None: app = client.apps.deploy( + entrypoint_rel_path="app.py", file=b"raw file contents", ) assert_matches_type(AppDeployResponse, app, path=["response"]) @@ -33,8 +34,8 @@ def test_method_deploy(self, client: Kernel) -> None: @parametrize def test_method_deploy_with_all_params(self, client: Kernel) -> None: app = client.apps.deploy( - file=b"raw file contents", entrypoint_rel_path="app.py", + file=b"raw file contents", force="false", region="aws.us-east-1a", version="1.0.0", @@ -45,6 +46,7 @@ def test_method_deploy_with_all_params(self, client: Kernel) -> None: @parametrize def test_raw_response_deploy(self, client: Kernel) -> None: response = client.apps.with_raw_response.deploy( + entrypoint_rel_path="app.py", file=b"raw file contents", ) @@ -57,6 +59,7 @@ def test_raw_response_deploy(self, client: Kernel) -> None: @parametrize def test_streaming_response_deploy(self, client: Kernel) -> None: with client.apps.with_streaming_response.deploy( + entrypoint_rel_path="app.py", file=b"raw file contents", ) as response: assert not response.is_closed @@ -160,6 +163,7 @@ class TestAsyncApps: @parametrize async def test_method_deploy(self, async_client: AsyncKernel) -> None: app = await async_client.apps.deploy( + entrypoint_rel_path="app.py", file=b"raw file contents", ) assert_matches_type(AppDeployResponse, app, path=["response"]) @@ -168,8 +172,8 @@ async def test_method_deploy(self, async_client: AsyncKernel) -> None: @parametrize async def test_method_deploy_with_all_params(self, async_client: AsyncKernel) -> None: app = await async_client.apps.deploy( - file=b"raw file contents", entrypoint_rel_path="app.py", + file=b"raw file contents", force="false", region="aws.us-east-1a", version="1.0.0", @@ -180,6 +184,7 @@ async def test_method_deploy_with_all_params(self, async_client: AsyncKernel) -> @parametrize async def test_raw_response_deploy(self, async_client: AsyncKernel) -> None: response = await async_client.apps.with_raw_response.deploy( + entrypoint_rel_path="app.py", file=b"raw file contents", ) @@ -192,6 +197,7 @@ async def test_raw_response_deploy(self, async_client: AsyncKernel) -> None: @parametrize async def test_streaming_response_deploy(self, async_client: AsyncKernel) -> None: async with async_client.apps.with_streaming_response.deploy( + entrypoint_rel_path="app.py", file=b"raw file contents", ) as response: assert not response.is_closed diff --git a/tests/test_client.py b/tests/test_client.py index 360b6228..713ce3c1 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -720,7 +720,12 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No with pytest.raises(APITimeoutError): self.client.post( "/apps/deploy", - body=cast(object, maybe_transform(dict(file=b"REPLACE_ME", version="REPLACE_ME"), AppDeployParams)), + body=cast( + object, + maybe_transform( + dict(entrypoint_rel_path="app.py", file=b"REPLACE_ME", version="REPLACE_ME"), AppDeployParams + ), + ), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -735,7 +740,12 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non with pytest.raises(APIStatusError): self.client.post( "/apps/deploy", - body=cast(object, maybe_transform(dict(file=b"REPLACE_ME", version="REPLACE_ME"), AppDeployParams)), + body=cast( + object, + maybe_transform( + dict(entrypoint_rel_path="app.py", file=b"REPLACE_ME", version="REPLACE_ME"), AppDeployParams + ), + ), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -768,7 +778,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/apps/deploy").mock(side_effect=retry_handler) - response = client.apps.with_raw_response.deploy(file=b"raw file contents") + response = client.apps.with_raw_response.deploy(entrypoint_rel_path="app.py", file=b"raw file contents") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -793,7 +803,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/apps/deploy").mock(side_effect=retry_handler) response = client.apps.with_raw_response.deploy( - file=b"raw file contents", extra_headers={"x-stainless-retry-count": Omit()} + entrypoint_rel_path="app.py", file=b"raw file contents", extra_headers={"x-stainless-retry-count": Omit()} ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -818,7 +828,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/apps/deploy").mock(side_effect=retry_handler) response = client.apps.with_raw_response.deploy( - file=b"raw file contents", extra_headers={"x-stainless-retry-count": "42"} + entrypoint_rel_path="app.py", file=b"raw file contents", extra_headers={"x-stainless-retry-count": "42"} ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1510,7 +1520,12 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) with pytest.raises(APITimeoutError): await self.client.post( "/apps/deploy", - body=cast(object, maybe_transform(dict(file=b"REPLACE_ME", version="REPLACE_ME"), AppDeployParams)), + body=cast( + object, + maybe_transform( + dict(entrypoint_rel_path="app.py", file=b"REPLACE_ME", version="REPLACE_ME"), AppDeployParams + ), + ), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -1525,7 +1540,12 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) with pytest.raises(APIStatusError): await self.client.post( "/apps/deploy", - body=cast(object, maybe_transform(dict(file=b"REPLACE_ME", version="REPLACE_ME"), AppDeployParams)), + body=cast( + object, + maybe_transform( + dict(entrypoint_rel_path="app.py", file=b"REPLACE_ME", version="REPLACE_ME"), AppDeployParams + ), + ), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -1559,7 +1579,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/apps/deploy").mock(side_effect=retry_handler) - response = await client.apps.with_raw_response.deploy(file=b"raw file contents") + response = await client.apps.with_raw_response.deploy(entrypoint_rel_path="app.py", file=b"raw file contents") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1585,7 +1605,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/apps/deploy").mock(side_effect=retry_handler) response = await client.apps.with_raw_response.deploy( - file=b"raw file contents", extra_headers={"x-stainless-retry-count": Omit()} + entrypoint_rel_path="app.py", file=b"raw file contents", extra_headers={"x-stainless-retry-count": Omit()} ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -1611,7 +1631,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/apps/deploy").mock(side_effect=retry_handler) response = await client.apps.with_raw_response.deploy( - file=b"raw file contents", extra_headers={"x-stainless-retry-count": "42"} + entrypoint_rel_path="app.py", file=b"raw file contents", extra_headers={"x-stainless-retry-count": "42"} ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" From 0d6c7dd1717a4e90aac7dffb2bfc3abe451cee5c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 22:55:24 +0000 Subject: [PATCH 027/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index c373724d..46b9b6b2 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.8" + ".": "0.1.0-alpha.9" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 37fb0034..5045418e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.1.0-alpha.8" +version = "0.1.0-alpha.9" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 924d7142..7716ecb9 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.1.0-alpha.8" # x-release-please-version +__version__ = "0.1.0-alpha.9" # x-release-please-version From 4999d0430f2c82fba3121ef8157fb05e798da474 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 15 May 2025 03:41:10 +0000 Subject: [PATCH 028/448] chore(ci): upload sdks to package manager --- .github/workflows/ci.yml | 24 ++++++++++++++++++++++++ scripts/utils/upload-artifact.sh | 25 +++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100755 scripts/utils/upload-artifact.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d9000de..51b16df4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,30 @@ jobs: - name: Run lints run: ./scripts/lint + upload: + if: github.repository == 'stainless-sdks/kernel-python' + timeout-minutes: 10 + name: upload + permissions: + contents: read + id-token: write + runs-on: depot-ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - name: Get GitHub OIDC Token + id: github-oidc + uses: actions/github-script@v6 + with: + script: core.setOutput('github_token', await core.getIDToken()); + + - name: Upload tarball + env: + URL: https://pkg.stainless.com/s + AUTH: ${{ steps.github-oidc.outputs.github_token }} + SHA: ${{ github.sha }} + run: ./scripts/utils/upload-artifact.sh + test: timeout-minutes: 10 name: test diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh new file mode 100755 index 00000000..42692db5 --- /dev/null +++ b/scripts/utils/upload-artifact.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -exuo pipefail + +RESPONSE=$(curl -X POST "$URL" \ + -H "Authorization: Bearer $AUTH" \ + -H "Content-Type: application/json") + +SIGNED_URL=$(echo "$RESPONSE" | jq -r '.url') + +if [[ "$SIGNED_URL" == "null" ]]; then + echo -e "\033[31mFailed to get signed URL.\033[0m" + exit 1 +fi + +UPLOAD_RESPONSE=$(tar -cz . | curl -v -X PUT \ + -H "Content-Type: application/gzip" \ + --data-binary @- "$SIGNED_URL" 2>&1) + +if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then + echo -e "\033[32mUploaded build to Stainless storage.\033[0m" + echo -e "\033[32mInstallation: npm install 'https://pkg.stainless.com/s/kernel-python/$SHA'\033[0m" +else + echo -e "\033[31mFailed to upload artifact.\033[0m" + exit 1 +fi From 60585dadf66e9fa393f4c97b865fefc565370d9b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 16 May 2025 03:02:48 +0000 Subject: [PATCH 029/448] chore(ci): fix installation instructions --- scripts/utils/upload-artifact.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh index 42692db5..7b344b4f 100755 --- a/scripts/utils/upload-artifact.sh +++ b/scripts/utils/upload-artifact.sh @@ -18,7 +18,7 @@ UPLOAD_RESPONSE=$(tar -cz . | curl -v -X PUT \ if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then echo -e "\033[32mUploaded build to Stainless storage.\033[0m" - echo -e "\033[32mInstallation: npm install 'https://pkg.stainless.com/s/kernel-python/$SHA'\033[0m" + echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/kernel-python/$SHA'\033[0m" else echo -e "\033[31mFailed to upload artifact.\033[0m" exit 1 From 1d3e98a84e66f4cae69496970c677c828658fd75 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 17 May 2025 02:52:34 +0000 Subject: [PATCH 030/448] chore(internal): codegen related update --- scripts/utils/upload-artifact.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh index 7b344b4f..c55ebbca 100755 --- a/scripts/utils/upload-artifact.sh +++ b/scripts/utils/upload-artifact.sh @@ -18,7 +18,7 @@ UPLOAD_RESPONSE=$(tar -cz . | curl -v -X PUT \ if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then echo -e "\033[32mUploaded build to Stainless storage.\033[0m" - echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/kernel-python/$SHA'\033[0m" + echo -e "\033[32mInstallation: pip install --pre 'https://pkg.stainless.com/s/kernel-python/$SHA'\033[0m" else echo -e "\033[31mFailed to upload artifact.\033[0m" exit 1 From 2901bb928d254c6daa465ae2ba848408b2014bc6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 18:37:53 +0000 Subject: [PATCH 031/448] feat(api): update via SDK Studio --- .stats.yml | 8 +- README.md | 54 +-- api.md | 28 +- src/kernel/_client.py | 19 +- src/kernel/resources/__init__.py | 26 +- src/kernel/resources/apps.py | 418 ------------------ src/kernel/resources/apps/__init__.py | 47 ++ src/kernel/resources/apps/apps.py | 134 ++++++ src/kernel/resources/apps/deployments.py | 224 ++++++++++ src/kernel/resources/apps/invocations.py | 280 ++++++++++++ src/kernel/resources/browser.py | 171 ------- src/kernel/resources/browsers.py | 248 +++++++++++ src/kernel/types/__init__.py | 10 +- src/kernel/types/app_deploy_response.py | 32 -- src/kernel/types/app_invoke_params.py | 23 - src/kernel/types/app_invoke_response.py | 19 - .../types/app_retrieve_invocation_response.py | 25 -- src/kernel/types/apps/__init__.py | 9 + .../deployment_create_params.py} | 13 +- .../types/apps/deployment_create_response.py | 35 ++ .../types/apps/invocation_create_params.py | 21 + .../types/apps/invocation_create_response.py | 25 ++ .../apps/invocation_retrieve_response.py | 47 ++ src/kernel/types/browser_create_params.py | 12 + ...response.py => browser_create_response.py} | 14 +- .../types/browser_create_session_params.py | 14 - src/kernel/types/browser_retrieve_response.py | 16 + tests/api_resources/apps/__init__.py | 1 + tests/api_resources/apps/test_deployments.py | 120 +++++ tests/api_resources/apps/test_invocations.py | 208 +++++++++ tests/api_resources/test_apps.py | 294 ------------ tests/api_resources/test_browser.py | 90 ---- tests/api_resources/test_browsers.py | 174 ++++++++ tests/test_client.py | 78 ++-- 34 files changed, 1715 insertions(+), 1222 deletions(-) delete mode 100644 src/kernel/resources/apps.py create mode 100644 src/kernel/resources/apps/__init__.py create mode 100644 src/kernel/resources/apps/apps.py create mode 100644 src/kernel/resources/apps/deployments.py create mode 100644 src/kernel/resources/apps/invocations.py delete mode 100644 src/kernel/resources/browser.py create mode 100644 src/kernel/resources/browsers.py delete mode 100644 src/kernel/types/app_deploy_response.py delete mode 100644 src/kernel/types/app_invoke_params.py delete mode 100644 src/kernel/types/app_invoke_response.py delete mode 100644 src/kernel/types/app_retrieve_invocation_response.py create mode 100644 src/kernel/types/apps/__init__.py rename src/kernel/types/{app_deploy_params.py => apps/deployment_create_params.py} (60%) create mode 100644 src/kernel/types/apps/deployment_create_response.py create mode 100644 src/kernel/types/apps/invocation_create_params.py create mode 100644 src/kernel/types/apps/invocation_create_response.py create mode 100644 src/kernel/types/apps/invocation_retrieve_response.py create mode 100644 src/kernel/types/browser_create_params.py rename src/kernel/types/{browser_create_session_response.py => browser_create_response.py} (62%) delete mode 100644 src/kernel/types/browser_create_session_params.py create mode 100644 src/kernel/types/browser_retrieve_response.py create mode 100644 tests/api_resources/apps/__init__.py create mode 100644 tests/api_resources/apps/test_deployments.py create mode 100644 tests/api_resources/apps/test_invocations.py delete mode 100644 tests/api_resources/test_apps.py delete mode 100644 tests/api_resources/test_browser.py create mode 100644 tests/api_resources/test_browsers.py diff --git a/.stats.yml b/.stats.yml index d6d797ad..6f21bcbb 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 4 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-07d481d1498bf9677437b555e9ec2d843d50107faa7501e4c430a32b1f3c3343.yml -openapi_spec_hash: 296f78d82afbac95fad12c5eabd71f18 -config_hash: 2c8351ba6611ce4a352e248405783846 +configured_endpoints: 5 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-f40e779e2a48f5e37361f2f4a9879e5c40f2851b8033c23db69ec7b91242bf69.yml +openapi_spec_hash: 2dfa146149e61363f1ec40bf9251eb7c +config_hash: 2ddaa85513b6670889b1a56c905423c7 diff --git a/README.md b/README.md index 801b34f8..1f5a5bb6 100644 --- a/README.md +++ b/README.md @@ -33,12 +33,12 @@ client = Kernel( environment="development", ) -response = client.apps.deploy( - entrypoint_rel_path="app.py", +deployment = client.apps.deployments.create( + entrypoint_rel_path="main.ts", file=b"REPLACE_ME", - version="REPLACE_ME", + version="1.0.0", ) -print(response.apps) +print(deployment.apps) ``` While you can provide an `api_key` keyword argument, @@ -63,12 +63,12 @@ client = AsyncKernel( async def main() -> None: - response = await client.apps.deploy( - entrypoint_rel_path="app.py", + deployment = await client.apps.deployments.create( + entrypoint_rel_path="main.ts", file=b"REPLACE_ME", - version="REPLACE_ME", + version="1.0.0", ) - print(response.apps) + print(deployment.apps) asyncio.run(main()) @@ -95,8 +95,8 @@ from kernel import Kernel client = Kernel() -client.apps.deploy( - entrypoint_rel_path="app.py", +client.apps.deployments.create( + entrypoint_rel_path="src/app.py", file=Path("/path/to/file"), ) ``` @@ -119,10 +119,8 @@ from kernel import Kernel client = Kernel() try: - client.apps.deploy( - entrypoint_rel_path="app.py", - file=b"REPLACE_ME", - version="REPLACE_ME", + client.browsers.create( + invocation_id="REPLACE_ME", ) except kernel.APIConnectionError as e: print("The server could not be reached") @@ -166,10 +164,8 @@ client = Kernel( ) # Or, configure per-request: -client.with_options(max_retries=5).apps.deploy( - entrypoint_rel_path="app.py", - file=b"REPLACE_ME", - version="REPLACE_ME", +client.with_options(max_retries=5).browsers.create( + invocation_id="REPLACE_ME", ) ``` @@ -193,10 +189,8 @@ client = Kernel( ) # Override per-request: -client.with_options(timeout=5.0).apps.deploy( - entrypoint_rel_path="app.py", - file=b"REPLACE_ME", - version="REPLACE_ME", +client.with_options(timeout=5.0).browsers.create( + invocation_id="REPLACE_ME", ) ``` @@ -238,15 +232,13 @@ The "raw" Response object can be accessed by prefixing `.with_raw_response.` to from kernel import Kernel client = Kernel() -response = client.apps.with_raw_response.deploy( - entrypoint_rel_path="app.py", - file=b"REPLACE_ME", - version="REPLACE_ME", +response = client.browsers.with_raw_response.create( + invocation_id="REPLACE_ME", ) print(response.headers.get('X-My-Header')) -app = response.parse() # get the object that `apps.deploy()` would have returned -print(app.apps) +browser = response.parse() # get the object that `browsers.create()` would have returned +print(browser.session_id) ``` These methods return an [`APIResponse`](https://github.com/onkernel/kernel-python-sdk/tree/main/src/kernel/_response.py) object. @@ -260,10 +252,8 @@ The above interface eagerly reads the full response body when you make the reque To stream the response body, use `.with_streaming_response` instead, which requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`. In the async client, these are async methods. ```python -with client.apps.with_streaming_response.deploy( - entrypoint_rel_path="app.py", - file=b"REPLACE_ME", - version="REPLACE_ME", +with client.browsers.with_streaming_response.create( + invocation_id="REPLACE_ME", ) as response: print(response.headers.get("X-My-Header")) diff --git a/api.md b/api.md index 581c76f6..3456b565 100644 --- a/api.md +++ b/api.md @@ -1,25 +1,39 @@ # Apps +## Deployments + +Types: + +```python +from kernel.types.apps import DeploymentCreateResponse +``` + +Methods: + +- client.apps.deployments.create(\*\*params) -> DeploymentCreateResponse + +## Invocations + Types: ```python -from kernel.types import AppDeployResponse, AppInvokeResponse, AppRetrieveInvocationResponse +from kernel.types.apps import InvocationCreateResponse, InvocationRetrieveResponse ``` Methods: -- client.apps.deploy(\*\*params) -> AppDeployResponse -- client.apps.invoke(\*\*params) -> AppInvokeResponse -- client.apps.retrieve_invocation(id) -> AppRetrieveInvocationResponse +- client.apps.invocations.create(\*\*params) -> InvocationCreateResponse +- client.apps.invocations.retrieve(id) -> InvocationRetrieveResponse -# Browser +# Browsers Types: ```python -from kernel.types import BrowserCreateSessionResponse +from kernel.types import BrowserCreateResponse, BrowserRetrieveResponse ``` Methods: -- client.browser.create_session(\*\*params) -> BrowserCreateSessionResponse +- client.browsers.create(\*\*params) -> BrowserCreateResponse +- client.browsers.retrieve(id) -> BrowserRetrieveResponse diff --git a/src/kernel/_client.py b/src/kernel/_client.py index 28871bad..bf6fbb4d 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -21,7 +21,7 @@ ) from ._utils import is_given, get_async_library from ._version import __version__ -from .resources import apps, browser +from .resources import browsers from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import KernelError, APIStatusError from ._base_client import ( @@ -29,6 +29,7 @@ SyncAPIClient, AsyncAPIClient, ) +from .resources.apps import apps __all__ = [ "ENVIRONMENTS", @@ -50,7 +51,7 @@ class Kernel(SyncAPIClient): apps: apps.AppsResource - browser: browser.BrowserResource + browsers: browsers.BrowsersResource with_raw_response: KernelWithRawResponse with_streaming_response: KernelWithStreamedResponse @@ -133,7 +134,7 @@ def __init__( ) self.apps = apps.AppsResource(self) - self.browser = browser.BrowserResource(self) + self.browsers = browsers.BrowsersResource(self) self.with_raw_response = KernelWithRawResponse(self) self.with_streaming_response = KernelWithStreamedResponse(self) @@ -246,7 +247,7 @@ def _make_status_error( class AsyncKernel(AsyncAPIClient): apps: apps.AsyncAppsResource - browser: browser.AsyncBrowserResource + browsers: browsers.AsyncBrowsersResource with_raw_response: AsyncKernelWithRawResponse with_streaming_response: AsyncKernelWithStreamedResponse @@ -329,7 +330,7 @@ def __init__( ) self.apps = apps.AsyncAppsResource(self) - self.browser = browser.AsyncBrowserResource(self) + self.browsers = browsers.AsyncBrowsersResource(self) self.with_raw_response = AsyncKernelWithRawResponse(self) self.with_streaming_response = AsyncKernelWithStreamedResponse(self) @@ -443,25 +444,25 @@ def _make_status_error( class KernelWithRawResponse: def __init__(self, client: Kernel) -> None: self.apps = apps.AppsResourceWithRawResponse(client.apps) - self.browser = browser.BrowserResourceWithRawResponse(client.browser) + self.browsers = browsers.BrowsersResourceWithRawResponse(client.browsers) class AsyncKernelWithRawResponse: def __init__(self, client: AsyncKernel) -> None: self.apps = apps.AsyncAppsResourceWithRawResponse(client.apps) - self.browser = browser.AsyncBrowserResourceWithRawResponse(client.browser) + self.browsers = browsers.AsyncBrowsersResourceWithRawResponse(client.browsers) class KernelWithStreamedResponse: def __init__(self, client: Kernel) -> None: self.apps = apps.AppsResourceWithStreamingResponse(client.apps) - self.browser = browser.BrowserResourceWithStreamingResponse(client.browser) + self.browsers = browsers.BrowsersResourceWithStreamingResponse(client.browsers) class AsyncKernelWithStreamedResponse: def __init__(self, client: AsyncKernel) -> None: self.apps = apps.AsyncAppsResourceWithStreamingResponse(client.apps) - self.browser = browser.AsyncBrowserResourceWithStreamingResponse(client.browser) + self.browsers = browsers.AsyncBrowsersResourceWithStreamingResponse(client.browsers) Client = Kernel diff --git a/src/kernel/resources/__init__.py b/src/kernel/resources/__init__.py index a0d1ea6f..647bde62 100644 --- a/src/kernel/resources/__init__.py +++ b/src/kernel/resources/__init__.py @@ -8,13 +8,13 @@ AppsResourceWithStreamingResponse, AsyncAppsResourceWithStreamingResponse, ) -from .browser import ( - BrowserResource, - AsyncBrowserResource, - BrowserResourceWithRawResponse, - AsyncBrowserResourceWithRawResponse, - BrowserResourceWithStreamingResponse, - AsyncBrowserResourceWithStreamingResponse, +from .browsers import ( + BrowsersResource, + AsyncBrowsersResource, + BrowsersResourceWithRawResponse, + AsyncBrowsersResourceWithRawResponse, + BrowsersResourceWithStreamingResponse, + AsyncBrowsersResourceWithStreamingResponse, ) __all__ = [ @@ -24,10 +24,10 @@ "AsyncAppsResourceWithRawResponse", "AppsResourceWithStreamingResponse", "AsyncAppsResourceWithStreamingResponse", - "BrowserResource", - "AsyncBrowserResource", - "BrowserResourceWithRawResponse", - "AsyncBrowserResourceWithRawResponse", - "BrowserResourceWithStreamingResponse", - "AsyncBrowserResourceWithStreamingResponse", + "BrowsersResource", + "AsyncBrowsersResource", + "BrowsersResourceWithRawResponse", + "AsyncBrowsersResourceWithRawResponse", + "BrowsersResourceWithStreamingResponse", + "AsyncBrowsersResourceWithStreamingResponse", ] diff --git a/src/kernel/resources/apps.py b/src/kernel/resources/apps.py deleted file mode 100644 index 023f2143..00000000 --- a/src/kernel/resources/apps.py +++ /dev/null @@ -1,418 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Mapping, cast -from typing_extensions import Literal - -import httpx - -from ..types import app_deploy_params, app_invoke_params -from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes -from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform -from .._compat import cached_property -from .._resource import SyncAPIResource, AsyncAPIResource -from .._response import ( - to_raw_response_wrapper, - to_streamed_response_wrapper, - async_to_raw_response_wrapper, - async_to_streamed_response_wrapper, -) -from .._base_client import make_request_options -from ..types.app_deploy_response import AppDeployResponse -from ..types.app_invoke_response import AppInvokeResponse -from ..types.app_retrieve_invocation_response import AppRetrieveInvocationResponse - -__all__ = ["AppsResource", "AsyncAppsResource"] - - -class AppsResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> AppsResourceWithRawResponse: - """ - 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers - """ - return AppsResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AppsResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response - """ - return AppsResourceWithStreamingResponse(self) - - def deploy( - self, - *, - entrypoint_rel_path: str, - file: FileTypes, - force: Literal["true", "false"] | NotGiven = NOT_GIVEN, - region: Literal["aws.us-east-1a"] | NotGiven = NOT_GIVEN, - version: 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, - ) -> AppDeployResponse: - """ - Deploy a new application - - Args: - entrypoint_rel_path: Relative path to the entrypoint of the application - - file: ZIP file containing the application source directory - - force: Allow overwriting an existing app version - - region: Region for deployment. Currently we only support "aws.us-east-1a" - - version: Version of the application. Can be any string. - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - body = deepcopy_minimal( - { - "entrypoint_rel_path": entrypoint_rel_path, - "file": file, - "force": force, - "region": region, - "version": version, - } - ) - files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) - # It should be noted that the actual Content-Type header that will be - # sent to the server will contain a `boundary` parameter, e.g. - # multipart/form-data; boundary=---abc-- - extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} - return self._post( - "/apps/deploy", - body=maybe_transform(body, app_deploy_params.AppDeployParams), - files=files, - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AppDeployResponse, - ) - - def invoke( - self, - *, - action_name: str, - app_name: str, - payload: object, - version: 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, - ) -> AppInvokeResponse: - """ - Invoke an application - - Args: - action_name: Name of the action to invoke - - app_name: Name of the application - - payload: Input data for the application - - version: Version of the application - - 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( - "/apps/invoke", - body=maybe_transform( - { - "action_name": action_name, - "app_name": app_name, - "payload": payload, - "version": version, - }, - app_invoke_params.AppInvokeParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AppInvokeResponse, - ) - - def retrieve_invocation( - self, - id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AppRetrieveInvocationResponse: - """ - Get an app invocation by id - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - return self._get( - f"/apps/invocations/{id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AppRetrieveInvocationResponse, - ) - - -class AsyncAppsResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncAppsResourceWithRawResponse: - """ - 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers - """ - return AsyncAppsResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncAppsResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response - """ - return AsyncAppsResourceWithStreamingResponse(self) - - async def deploy( - self, - *, - entrypoint_rel_path: str, - file: FileTypes, - force: Literal["true", "false"] | NotGiven = NOT_GIVEN, - region: Literal["aws.us-east-1a"] | NotGiven = NOT_GIVEN, - version: 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, - ) -> AppDeployResponse: - """ - Deploy a new application - - Args: - entrypoint_rel_path: Relative path to the entrypoint of the application - - file: ZIP file containing the application source directory - - force: Allow overwriting an existing app version - - region: Region for deployment. Currently we only support "aws.us-east-1a" - - version: Version of the application. Can be any string. - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - body = deepcopy_minimal( - { - "entrypoint_rel_path": entrypoint_rel_path, - "file": file, - "force": force, - "region": region, - "version": version, - } - ) - files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) - # It should be noted that the actual Content-Type header that will be - # sent to the server will contain a `boundary` parameter, e.g. - # multipart/form-data; boundary=---abc-- - extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} - return await self._post( - "/apps/deploy", - body=await async_maybe_transform(body, app_deploy_params.AppDeployParams), - files=files, - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AppDeployResponse, - ) - - async def invoke( - self, - *, - action_name: str, - app_name: str, - payload: object, - version: 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, - ) -> AppInvokeResponse: - """ - Invoke an application - - Args: - action_name: Name of the action to invoke - - app_name: Name of the application - - payload: Input data for the application - - version: Version of the application - - 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( - "/apps/invoke", - body=await async_maybe_transform( - { - "action_name": action_name, - "app_name": app_name, - "payload": payload, - "version": version, - }, - app_invoke_params.AppInvokeParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AppInvokeResponse, - ) - - async def retrieve_invocation( - self, - id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AppRetrieveInvocationResponse: - """ - Get an app invocation by id - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - return await self._get( - f"/apps/invocations/{id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AppRetrieveInvocationResponse, - ) - - -class AppsResourceWithRawResponse: - def __init__(self, apps: AppsResource) -> None: - self._apps = apps - - self.deploy = to_raw_response_wrapper( - apps.deploy, - ) - self.invoke = to_raw_response_wrapper( - apps.invoke, - ) - self.retrieve_invocation = to_raw_response_wrapper( - apps.retrieve_invocation, - ) - - -class AsyncAppsResourceWithRawResponse: - def __init__(self, apps: AsyncAppsResource) -> None: - self._apps = apps - - self.deploy = async_to_raw_response_wrapper( - apps.deploy, - ) - self.invoke = async_to_raw_response_wrapper( - apps.invoke, - ) - self.retrieve_invocation = async_to_raw_response_wrapper( - apps.retrieve_invocation, - ) - - -class AppsResourceWithStreamingResponse: - def __init__(self, apps: AppsResource) -> None: - self._apps = apps - - self.deploy = to_streamed_response_wrapper( - apps.deploy, - ) - self.invoke = to_streamed_response_wrapper( - apps.invoke, - ) - self.retrieve_invocation = to_streamed_response_wrapper( - apps.retrieve_invocation, - ) - - -class AsyncAppsResourceWithStreamingResponse: - def __init__(self, apps: AsyncAppsResource) -> None: - self._apps = apps - - self.deploy = async_to_streamed_response_wrapper( - apps.deploy, - ) - self.invoke = async_to_streamed_response_wrapper( - apps.invoke, - ) - self.retrieve_invocation = async_to_streamed_response_wrapper( - apps.retrieve_invocation, - ) diff --git a/src/kernel/resources/apps/__init__.py b/src/kernel/resources/apps/__init__.py new file mode 100644 index 00000000..5602ad74 --- /dev/null +++ b/src/kernel/resources/apps/__init__.py @@ -0,0 +1,47 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .apps import ( + AppsResource, + AsyncAppsResource, + AppsResourceWithRawResponse, + AsyncAppsResourceWithRawResponse, + AppsResourceWithStreamingResponse, + AsyncAppsResourceWithStreamingResponse, +) +from .deployments import ( + DeploymentsResource, + AsyncDeploymentsResource, + DeploymentsResourceWithRawResponse, + AsyncDeploymentsResourceWithRawResponse, + DeploymentsResourceWithStreamingResponse, + AsyncDeploymentsResourceWithStreamingResponse, +) +from .invocations import ( + InvocationsResource, + AsyncInvocationsResource, + InvocationsResourceWithRawResponse, + AsyncInvocationsResourceWithRawResponse, + InvocationsResourceWithStreamingResponse, + AsyncInvocationsResourceWithStreamingResponse, +) + +__all__ = [ + "DeploymentsResource", + "AsyncDeploymentsResource", + "DeploymentsResourceWithRawResponse", + "AsyncDeploymentsResourceWithRawResponse", + "DeploymentsResourceWithStreamingResponse", + "AsyncDeploymentsResourceWithStreamingResponse", + "InvocationsResource", + "AsyncInvocationsResource", + "InvocationsResourceWithRawResponse", + "AsyncInvocationsResourceWithRawResponse", + "InvocationsResourceWithStreamingResponse", + "AsyncInvocationsResourceWithStreamingResponse", + "AppsResource", + "AsyncAppsResource", + "AppsResourceWithRawResponse", + "AsyncAppsResourceWithRawResponse", + "AppsResourceWithStreamingResponse", + "AsyncAppsResourceWithStreamingResponse", +] diff --git a/src/kernel/resources/apps/apps.py b/src/kernel/resources/apps/apps.py new file mode 100644 index 00000000..848b765b --- /dev/null +++ b/src/kernel/resources/apps/apps.py @@ -0,0 +1,134 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from .deployments import ( + DeploymentsResource, + AsyncDeploymentsResource, + DeploymentsResourceWithRawResponse, + AsyncDeploymentsResourceWithRawResponse, + DeploymentsResourceWithStreamingResponse, + AsyncDeploymentsResourceWithStreamingResponse, +) +from .invocations import ( + InvocationsResource, + AsyncInvocationsResource, + InvocationsResourceWithRawResponse, + AsyncInvocationsResourceWithRawResponse, + InvocationsResourceWithStreamingResponse, + AsyncInvocationsResourceWithStreamingResponse, +) + +__all__ = ["AppsResource", "AsyncAppsResource"] + + +class AppsResource(SyncAPIResource): + @cached_property + def deployments(self) -> DeploymentsResource: + return DeploymentsResource(self._client) + + @cached_property + def invocations(self) -> InvocationsResource: + return InvocationsResource(self._client) + + @cached_property + def with_raw_response(self) -> AppsResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AppsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AppsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return AppsResourceWithStreamingResponse(self) + + +class AsyncAppsResource(AsyncAPIResource): + @cached_property + def deployments(self) -> AsyncDeploymentsResource: + return AsyncDeploymentsResource(self._client) + + @cached_property + def invocations(self) -> AsyncInvocationsResource: + return AsyncInvocationsResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncAppsResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncAppsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAppsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return AsyncAppsResourceWithStreamingResponse(self) + + +class AppsResourceWithRawResponse: + def __init__(self, apps: AppsResource) -> None: + self._apps = apps + + @cached_property + def deployments(self) -> DeploymentsResourceWithRawResponse: + return DeploymentsResourceWithRawResponse(self._apps.deployments) + + @cached_property + def invocations(self) -> InvocationsResourceWithRawResponse: + return InvocationsResourceWithRawResponse(self._apps.invocations) + + +class AsyncAppsResourceWithRawResponse: + def __init__(self, apps: AsyncAppsResource) -> None: + self._apps = apps + + @cached_property + def deployments(self) -> AsyncDeploymentsResourceWithRawResponse: + return AsyncDeploymentsResourceWithRawResponse(self._apps.deployments) + + @cached_property + def invocations(self) -> AsyncInvocationsResourceWithRawResponse: + return AsyncInvocationsResourceWithRawResponse(self._apps.invocations) + + +class AppsResourceWithStreamingResponse: + def __init__(self, apps: AppsResource) -> None: + self._apps = apps + + @cached_property + def deployments(self) -> DeploymentsResourceWithStreamingResponse: + return DeploymentsResourceWithStreamingResponse(self._apps.deployments) + + @cached_property + def invocations(self) -> InvocationsResourceWithStreamingResponse: + return InvocationsResourceWithStreamingResponse(self._apps.invocations) + + +class AsyncAppsResourceWithStreamingResponse: + def __init__(self, apps: AsyncAppsResource) -> None: + self._apps = apps + + @cached_property + def deployments(self) -> AsyncDeploymentsResourceWithStreamingResponse: + return AsyncDeploymentsResourceWithStreamingResponse(self._apps.deployments) + + @cached_property + def invocations(self) -> AsyncInvocationsResourceWithStreamingResponse: + return AsyncInvocationsResourceWithStreamingResponse(self._apps.invocations) diff --git a/src/kernel/resources/apps/deployments.py b/src/kernel/resources/apps/deployments.py new file mode 100644 index 00000000..0b88fd1e --- /dev/null +++ b/src/kernel/resources/apps/deployments.py @@ -0,0 +1,224 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Mapping, cast +from typing_extensions import Literal + +import httpx + +from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes +from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ...types.apps import deployment_create_params +from ..._base_client import make_request_options +from ...types.apps.deployment_create_response import DeploymentCreateResponse + +__all__ = ["DeploymentsResource", "AsyncDeploymentsResource"] + + +class DeploymentsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> DeploymentsResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return DeploymentsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> DeploymentsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return DeploymentsResourceWithStreamingResponse(self) + + def create( + self, + *, + entrypoint_rel_path: str, + file: FileTypes, + force: bool | NotGiven = NOT_GIVEN, + region: Literal["aws.us-east-1a"] | NotGiven = NOT_GIVEN, + version: 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, + ) -> DeploymentCreateResponse: + """ + Deploy a new application + + Args: + entrypoint_rel_path: Relative path to the entrypoint of the application + + file: ZIP file containing the application source directory + + force: Allow overwriting an existing app version + + region: Region for deployment. Currently we only support "aws.us-east-1a" + + version: Version of the application. Can be any string. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "entrypoint_rel_path": entrypoint_rel_path, + "file": file, + "force": force, + "region": region, + "version": version, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return self._post( + "/deploy", + body=maybe_transform(body, deployment_create_params.DeploymentCreateParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=DeploymentCreateResponse, + ) + + +class AsyncDeploymentsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncDeploymentsResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncDeploymentsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncDeploymentsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return AsyncDeploymentsResourceWithStreamingResponse(self) + + async def create( + self, + *, + entrypoint_rel_path: str, + file: FileTypes, + force: bool | NotGiven = NOT_GIVEN, + region: Literal["aws.us-east-1a"] | NotGiven = NOT_GIVEN, + version: 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, + ) -> DeploymentCreateResponse: + """ + Deploy a new application + + Args: + entrypoint_rel_path: Relative path to the entrypoint of the application + + file: ZIP file containing the application source directory + + force: Allow overwriting an existing app version + + region: Region for deployment. Currently we only support "aws.us-east-1a" + + version: Version of the application. Can be any string. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "entrypoint_rel_path": entrypoint_rel_path, + "file": file, + "force": force, + "region": region, + "version": version, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return await self._post( + "/deploy", + body=await async_maybe_transform(body, deployment_create_params.DeploymentCreateParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=DeploymentCreateResponse, + ) + + +class DeploymentsResourceWithRawResponse: + def __init__(self, deployments: DeploymentsResource) -> None: + self._deployments = deployments + + self.create = to_raw_response_wrapper( + deployments.create, + ) + + +class AsyncDeploymentsResourceWithRawResponse: + def __init__(self, deployments: AsyncDeploymentsResource) -> None: + self._deployments = deployments + + self.create = async_to_raw_response_wrapper( + deployments.create, + ) + + +class DeploymentsResourceWithStreamingResponse: + def __init__(self, deployments: DeploymentsResource) -> None: + self._deployments = deployments + + self.create = to_streamed_response_wrapper( + deployments.create, + ) + + +class AsyncDeploymentsResourceWithStreamingResponse: + def __init__(self, deployments: AsyncDeploymentsResource) -> None: + self._deployments = deployments + + self.create = async_to_streamed_response_wrapper( + deployments.create, + ) diff --git a/src/kernel/resources/apps/invocations.py b/src/kernel/resources/apps/invocations.py new file mode 100644 index 00000000..44015013 --- /dev/null +++ b/src/kernel/resources/apps/invocations.py @@ -0,0 +1,280 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ..._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 ...types.apps import invocation_create_params +from ..._base_client import make_request_options +from ...types.apps.invocation_create_response import InvocationCreateResponse +from ...types.apps.invocation_retrieve_response import InvocationRetrieveResponse + +__all__ = ["InvocationsResource", "AsyncInvocationsResource"] + + +class InvocationsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> InvocationsResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return InvocationsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> InvocationsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return InvocationsResourceWithStreamingResponse(self) + + def create( + self, + *, + action_name: str, + app_name: str, + version: str, + payload: 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, + ) -> InvocationCreateResponse: + """ + Invoke an application + + Args: + action_name: Name of the action to invoke + + app_name: Name of the application + + version: Version of the application + + payload: Input data for the action, sent as a JSON string. + + 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( + "/invocations", + body=maybe_transform( + { + "action_name": action_name, + "app_name": app_name, + "version": version, + "payload": payload, + }, + invocation_create_params.InvocationCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=InvocationCreateResponse, + ) + + def retrieve( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> InvocationRetrieveResponse: + """ + Get an app invocation by id + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/invocations/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=InvocationRetrieveResponse, + ) + + +class AsyncInvocationsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncInvocationsResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncInvocationsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncInvocationsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return AsyncInvocationsResourceWithStreamingResponse(self) + + async def create( + self, + *, + action_name: str, + app_name: str, + version: str, + payload: 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, + ) -> InvocationCreateResponse: + """ + Invoke an application + + Args: + action_name: Name of the action to invoke + + app_name: Name of the application + + version: Version of the application + + payload: Input data for the action, sent as a JSON string. + + 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( + "/invocations", + body=await async_maybe_transform( + { + "action_name": action_name, + "app_name": app_name, + "version": version, + "payload": payload, + }, + invocation_create_params.InvocationCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=InvocationCreateResponse, + ) + + async def retrieve( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> InvocationRetrieveResponse: + """ + Get an app invocation by id + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/invocations/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=InvocationRetrieveResponse, + ) + + +class InvocationsResourceWithRawResponse: + def __init__(self, invocations: InvocationsResource) -> None: + self._invocations = invocations + + self.create = to_raw_response_wrapper( + invocations.create, + ) + self.retrieve = to_raw_response_wrapper( + invocations.retrieve, + ) + + +class AsyncInvocationsResourceWithRawResponse: + def __init__(self, invocations: AsyncInvocationsResource) -> None: + self._invocations = invocations + + self.create = async_to_raw_response_wrapper( + invocations.create, + ) + self.retrieve = async_to_raw_response_wrapper( + invocations.retrieve, + ) + + +class InvocationsResourceWithStreamingResponse: + def __init__(self, invocations: InvocationsResource) -> None: + self._invocations = invocations + + self.create = to_streamed_response_wrapper( + invocations.create, + ) + self.retrieve = to_streamed_response_wrapper( + invocations.retrieve, + ) + + +class AsyncInvocationsResourceWithStreamingResponse: + def __init__(self, invocations: AsyncInvocationsResource) -> None: + self._invocations = invocations + + self.create = async_to_streamed_response_wrapper( + invocations.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + invocations.retrieve, + ) diff --git a/src/kernel/resources/browser.py b/src/kernel/resources/browser.py deleted file mode 100644 index 3edf8c08..00000000 --- a/src/kernel/resources/browser.py +++ /dev/null @@ -1,171 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import httpx - -from ..types import browser_create_session_params -from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from .._utils import maybe_transform, async_maybe_transform -from .._compat import cached_property -from .._resource import SyncAPIResource, AsyncAPIResource -from .._response import ( - to_raw_response_wrapper, - to_streamed_response_wrapper, - async_to_raw_response_wrapper, - async_to_streamed_response_wrapper, -) -from .._base_client import make_request_options -from ..types.browser_create_session_response import BrowserCreateSessionResponse - -__all__ = ["BrowserResource", "AsyncBrowserResource"] - - -class BrowserResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> BrowserResourceWithRawResponse: - """ - 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers - """ - return BrowserResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> BrowserResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response - """ - return BrowserResourceWithStreamingResponse(self) - - def create_session( - self, - *, - invocation_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, - ) -> BrowserCreateSessionResponse: - """ - Create Browser Session - - Args: - invocation_id: Kernel App invocation ID - - 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", - body=maybe_transform( - {"invocation_id": invocation_id}, browser_create_session_params.BrowserCreateSessionParams - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=BrowserCreateSessionResponse, - ) - - -class AsyncBrowserResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncBrowserResourceWithRawResponse: - """ - 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers - """ - return AsyncBrowserResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncBrowserResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response - """ - return AsyncBrowserResourceWithStreamingResponse(self) - - async def create_session( - self, - *, - invocation_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, - ) -> BrowserCreateSessionResponse: - """ - Create Browser Session - - Args: - invocation_id: Kernel App invocation ID - - 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", - body=await async_maybe_transform( - {"invocation_id": invocation_id}, browser_create_session_params.BrowserCreateSessionParams - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=BrowserCreateSessionResponse, - ) - - -class BrowserResourceWithRawResponse: - def __init__(self, browser: BrowserResource) -> None: - self._browser = browser - - self.create_session = to_raw_response_wrapper( - browser.create_session, - ) - - -class AsyncBrowserResourceWithRawResponse: - def __init__(self, browser: AsyncBrowserResource) -> None: - self._browser = browser - - self.create_session = async_to_raw_response_wrapper( - browser.create_session, - ) - - -class BrowserResourceWithStreamingResponse: - def __init__(self, browser: BrowserResource) -> None: - self._browser = browser - - self.create_session = to_streamed_response_wrapper( - browser.create_session, - ) - - -class AsyncBrowserResourceWithStreamingResponse: - def __init__(self, browser: AsyncBrowserResource) -> None: - self._browser = browser - - self.create_session = async_to_streamed_response_wrapper( - browser.create_session, - ) diff --git a/src/kernel/resources/browsers.py b/src/kernel/resources/browsers.py new file mode 100644 index 00000000..2aa307a8 --- /dev/null +++ b/src/kernel/resources/browsers.py @@ -0,0 +1,248 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..types import browser_create_params +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._utils import maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.browser_create_response import BrowserCreateResponse +from ..types.browser_retrieve_response import BrowserRetrieveResponse + +__all__ = ["BrowsersResource", "AsyncBrowsersResource"] + + +class BrowsersResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> BrowsersResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return BrowsersResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> BrowsersResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return BrowsersResourceWithStreamingResponse(self) + + def create( + self, + *, + invocation_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, + ) -> BrowserCreateResponse: + """ + Create Browser Session + + Args: + invocation_id: action invocation ID + + 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( + "/browsers", + body=maybe_transform({"invocation_id": invocation_id}, browser_create_params.BrowserCreateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BrowserCreateResponse, + ) + + def retrieve( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BrowserRetrieveResponse: + """ + Get Browser Session by ID + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/browsers/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BrowserRetrieveResponse, + ) + + +class AsyncBrowsersResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncBrowsersResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncBrowsersResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncBrowsersResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return AsyncBrowsersResourceWithStreamingResponse(self) + + async def create( + self, + *, + invocation_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, + ) -> BrowserCreateResponse: + """ + Create Browser Session + + Args: + invocation_id: action invocation ID + + 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( + "/browsers", + body=await async_maybe_transform( + {"invocation_id": invocation_id}, browser_create_params.BrowserCreateParams + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BrowserCreateResponse, + ) + + async def retrieve( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BrowserRetrieveResponse: + """ + Get Browser Session by ID + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/browsers/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BrowserRetrieveResponse, + ) + + +class BrowsersResourceWithRawResponse: + def __init__(self, browsers: BrowsersResource) -> None: + self._browsers = browsers + + self.create = to_raw_response_wrapper( + browsers.create, + ) + self.retrieve = to_raw_response_wrapper( + browsers.retrieve, + ) + + +class AsyncBrowsersResourceWithRawResponse: + def __init__(self, browsers: AsyncBrowsersResource) -> None: + self._browsers = browsers + + self.create = async_to_raw_response_wrapper( + browsers.create, + ) + self.retrieve = async_to_raw_response_wrapper( + browsers.retrieve, + ) + + +class BrowsersResourceWithStreamingResponse: + def __init__(self, browsers: BrowsersResource) -> None: + self._browsers = browsers + + self.create = to_streamed_response_wrapper( + browsers.create, + ) + self.retrieve = to_streamed_response_wrapper( + browsers.retrieve, + ) + + +class AsyncBrowsersResourceWithStreamingResponse: + def __init__(self, browsers: AsyncBrowsersResource) -> None: + self._browsers = browsers + + self.create = async_to_streamed_response_wrapper( + browsers.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + browsers.retrieve, + ) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 32a47687..282c8899 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -2,10 +2,6 @@ from __future__ import annotations -from .app_deploy_params import AppDeployParams as AppDeployParams -from .app_invoke_params import AppInvokeParams as AppInvokeParams -from .app_deploy_response import AppDeployResponse as AppDeployResponse -from .app_invoke_response import AppInvokeResponse as AppInvokeResponse -from .browser_create_session_params import BrowserCreateSessionParams as BrowserCreateSessionParams -from .browser_create_session_response import BrowserCreateSessionResponse as BrowserCreateSessionResponse -from .app_retrieve_invocation_response import AppRetrieveInvocationResponse as AppRetrieveInvocationResponse +from .browser_create_params import BrowserCreateParams as BrowserCreateParams +from .browser_create_response import BrowserCreateResponse as BrowserCreateResponse +from .browser_retrieve_response import BrowserRetrieveResponse as BrowserRetrieveResponse diff --git a/src/kernel/types/app_deploy_response.py b/src/kernel/types/app_deploy_response.py deleted file mode 100644 index e82164ea..00000000 --- a/src/kernel/types/app_deploy_response.py +++ /dev/null @@ -1,32 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List - -from .._models import BaseModel - -__all__ = ["AppDeployResponse", "App", "AppAction"] - - -class AppAction(BaseModel): - name: str - """Name of the action""" - - -class App(BaseModel): - id: str - """ID for the app version deployed""" - - actions: List[AppAction] - - name: str - """Name of the app""" - - -class AppDeployResponse(BaseModel): - apps: List[App] - - message: str - """Success message""" - - success: bool - """Status of the deployment""" diff --git a/src/kernel/types/app_invoke_params.py b/src/kernel/types/app_invoke_params.py deleted file mode 100644 index 414da988..00000000 --- a/src/kernel/types/app_invoke_params.py +++ /dev/null @@ -1,23 +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 - -__all__ = ["AppInvokeParams"] - - -class AppInvokeParams(TypedDict, total=False): - action_name: Required[Annotated[str, PropertyInfo(alias="actionName")]] - """Name of the action to invoke""" - - app_name: Required[Annotated[str, PropertyInfo(alias="appName")]] - """Name of the application""" - - payload: Required[object] - """Input data for the application""" - - version: Required[str] - """Version of the application""" diff --git a/src/kernel/types/app_invoke_response.py b/src/kernel/types/app_invoke_response.py deleted file mode 100644 index e76a9fd5..00000000 --- a/src/kernel/types/app_invoke_response.py +++ /dev/null @@ -1,19 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional -from typing_extensions import Literal - -from .._models import BaseModel - -__all__ = ["AppInvokeResponse"] - - -class AppInvokeResponse(BaseModel): - id: str - """ID of the invocation""" - - status: Literal["QUEUED", "RUNNING", "SUCCEEDED", "FAILED"] - """Status of the invocation""" - - output: Optional[str] = None - """Output from the invocation (if available)""" diff --git a/src/kernel/types/app_retrieve_invocation_response.py b/src/kernel/types/app_retrieve_invocation_response.py deleted file mode 100644 index 8b3de1f8..00000000 --- a/src/kernel/types/app_retrieve_invocation_response.py +++ /dev/null @@ -1,25 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["AppRetrieveInvocationResponse"] - - -class AppRetrieveInvocationResponse(BaseModel): - id: str - - app_name: str = FieldInfo(alias="appName") - - finished_at: Optional[str] = FieldInfo(alias="finishedAt", default=None) - - input: str - - output: str - - started_at: str = FieldInfo(alias="startedAt") - - status: str diff --git a/src/kernel/types/apps/__init__.py b/src/kernel/types/apps/__init__.py new file mode 100644 index 00000000..f4d451cf --- /dev/null +++ b/src/kernel/types/apps/__init__.py @@ -0,0 +1,9 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .deployment_create_params import DeploymentCreateParams as DeploymentCreateParams +from .invocation_create_params import InvocationCreateParams as InvocationCreateParams +from .deployment_create_response import DeploymentCreateResponse as DeploymentCreateResponse +from .invocation_create_response import InvocationCreateResponse as InvocationCreateResponse +from .invocation_retrieve_response import InvocationRetrieveResponse as InvocationRetrieveResponse diff --git a/src/kernel/types/app_deploy_params.py b/src/kernel/types/apps/deployment_create_params.py similarity index 60% rename from src/kernel/types/app_deploy_params.py rename to src/kernel/types/apps/deployment_create_params.py index 790743db..92ff2586 100644 --- a/src/kernel/types/app_deploy_params.py +++ b/src/kernel/types/apps/deployment_create_params.py @@ -2,22 +2,21 @@ from __future__ import annotations -from typing_extensions import Literal, Required, Annotated, TypedDict +from typing_extensions import Literal, Required, TypedDict -from .._types import FileTypes -from .._utils import PropertyInfo +from ..._types import FileTypes -__all__ = ["AppDeployParams"] +__all__ = ["DeploymentCreateParams"] -class AppDeployParams(TypedDict, total=False): - entrypoint_rel_path: Required[Annotated[str, PropertyInfo(alias="entrypointRelPath")]] +class DeploymentCreateParams(TypedDict, total=False): + entrypoint_rel_path: Required[str] """Relative path to the entrypoint of the application""" file: Required[FileTypes] """ZIP file containing the application source directory""" - force: Literal["true", "false"] + force: bool """Allow overwriting an existing app version""" region: Literal["aws.us-east-1a"] diff --git a/src/kernel/types/apps/deployment_create_response.py b/src/kernel/types/apps/deployment_create_response.py new file mode 100644 index 00000000..f801195c --- /dev/null +++ b/src/kernel/types/apps/deployment_create_response.py @@ -0,0 +1,35 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["DeploymentCreateResponse", "App", "AppAction"] + + +class AppAction(BaseModel): + name: str + """Name of the action""" + + +class App(BaseModel): + id: str + """ID for the app version deployed""" + + actions: List[AppAction] + """List of actions available on the app""" + + name: str + """Name of the app""" + + +class DeploymentCreateResponse(BaseModel): + apps: List[App] + """List of apps deployed""" + + status: Literal["queued", "deploying", "succeeded", "failed"] + """Current status of the deployment""" + + status_reason: Optional[str] = None + """Status reason""" diff --git a/src/kernel/types/apps/invocation_create_params.py b/src/kernel/types/apps/invocation_create_params.py new file mode 100644 index 00000000..a97a2c5a --- /dev/null +++ b/src/kernel/types/apps/invocation_create_params.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["InvocationCreateParams"] + + +class InvocationCreateParams(TypedDict, total=False): + action_name: Required[str] + """Name of the action to invoke""" + + app_name: Required[str] + """Name of the application""" + + version: Required[str] + """Version of the application""" + + payload: str + """Input data for the action, sent as a JSON string.""" diff --git a/src/kernel/types/apps/invocation_create_response.py b/src/kernel/types/apps/invocation_create_response.py new file mode 100644 index 00000000..df4a1664 --- /dev/null +++ b/src/kernel/types/apps/invocation_create_response.py @@ -0,0 +1,25 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["InvocationCreateResponse"] + + +class InvocationCreateResponse(BaseModel): + id: str + """ID of the invocation""" + + status: Literal["queued", "running", "succeeded", "failed"] + """Status of the invocation""" + + output: Optional[str] = None + """The return value of the action that was invoked, rendered as a JSON string. + + This could be: string, number, boolean, array, object, or null. + """ + + status_reason: Optional[str] = None + """Status reason""" diff --git a/src/kernel/types/apps/invocation_retrieve_response.py b/src/kernel/types/apps/invocation_retrieve_response.py new file mode 100644 index 00000000..f328b146 --- /dev/null +++ b/src/kernel/types/apps/invocation_retrieve_response.py @@ -0,0 +1,47 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["InvocationRetrieveResponse"] + + +class InvocationRetrieveResponse(BaseModel): + id: str + """ID of the invocation""" + + action_name: str + """Name of the action invoked""" + + app_name: str + """Name of the application""" + + started_at: datetime + """RFC 3339 Nanoseconds timestamp when the invocation started""" + + status: Literal["queued", "running", "succeeded", "failed"] + """Status of the invocation""" + + finished_at: Optional[datetime] = None + """ + RFC 3339 Nanoseconds timestamp when the invocation finished (null if still + running) + """ + + output: Optional[str] = None + """Output produced by the action, rendered as a JSON string. + + This could be: string, number, boolean, array, object, or null. + """ + + payload: Optional[str] = None + """Payload provided to the invocation. + + This is a string that can be parsed as JSON. + """ + + status_reason: Optional[str] = None + """Status reason""" diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py new file mode 100644 index 00000000..0944a61e --- /dev/null +++ b/src/kernel/types/browser_create_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["BrowserCreateParams"] + + +class BrowserCreateParams(TypedDict, total=False): + invocation_id: Required[str] + """action invocation ID""" diff --git a/src/kernel/types/browser_create_session_response.py b/src/kernel/types/browser_create_response.py similarity index 62% rename from src/kernel/types/browser_create_session_response.py rename to src/kernel/types/browser_create_response.py index d4e46da8..647dfc86 100644 --- a/src/kernel/types/browser_create_session_response.py +++ b/src/kernel/types/browser_create_response.py @@ -1,18 +1,16 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from pydantic import Field as FieldInfo - from .._models import BaseModel -__all__ = ["BrowserCreateSessionResponse"] +__all__ = ["BrowserCreateResponse"] + +class BrowserCreateResponse(BaseModel): + browser_live_view_url: str + """Remote URL for live viewing the browser session""" -class BrowserCreateSessionResponse(BaseModel): cdp_ws_url: str """Websocket URL for Chrome DevTools Protocol connections to the browser session""" - remote_url: str - """Remote URL for live viewing the browser session""" - - session_id: str = FieldInfo(alias="sessionId") + session_id: str """Unique identifier for the browser session""" diff --git a/src/kernel/types/browser_create_session_params.py b/src/kernel/types/browser_create_session_params.py deleted file mode 100644 index 73389bee..00000000 --- a/src/kernel/types/browser_create_session_params.py +++ /dev/null @@ -1,14 +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 - -__all__ = ["BrowserCreateSessionParams"] - - -class BrowserCreateSessionParams(TypedDict, total=False): - invocation_id: Required[Annotated[str, PropertyInfo(alias="invocationId")]] - """Kernel App invocation ID""" diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py new file mode 100644 index 00000000..e84bb015 --- /dev/null +++ b/src/kernel/types/browser_retrieve_response.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["BrowserRetrieveResponse"] + + +class BrowserRetrieveResponse(BaseModel): + browser_live_view_url: str + """Remote URL for live viewing the browser session""" + + cdp_ws_url: str + """Websocket URL for Chrome DevTools Protocol connections to the browser session""" + + session_id: str + """Unique identifier for the browser session""" diff --git a/tests/api_resources/apps/__init__.py b/tests/api_resources/apps/__init__.py new file mode 100644 index 00000000..fd8019a9 --- /dev/null +++ b/tests/api_resources/apps/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/apps/test_deployments.py b/tests/api_resources/apps/test_deployments.py new file mode 100644 index 00000000..e2f2a3dd --- /dev/null +++ b/tests/api_resources/apps/test_deployments.py @@ -0,0 +1,120 @@ +# 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 kernel import Kernel, AsyncKernel +from tests.utils import assert_matches_type +from kernel.types.apps import DeploymentCreateResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestDeployments: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip() + @parametrize + def test_method_create(self, client: Kernel) -> None: + deployment = client.apps.deployments.create( + entrypoint_rel_path="src/app.py", + file=b"raw file contents", + ) + assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_method_create_with_all_params(self, client: Kernel) -> None: + deployment = client.apps.deployments.create( + entrypoint_rel_path="src/app.py", + file=b"raw file contents", + force=False, + region="aws.us-east-1a", + version="1.0.0", + ) + assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_raw_response_create(self, client: Kernel) -> None: + response = client.apps.deployments.with_raw_response.create( + entrypoint_rel_path="src/app.py", + file=b"raw file contents", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + deployment = response.parse() + assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_streaming_response_create(self, client: Kernel) -> None: + with client.apps.deployments.with_streaming_response.create( + entrypoint_rel_path="src/app.py", + file=b"raw file contents", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + deployment = response.parse() + assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncDeployments: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip() + @parametrize + async def test_method_create(self, async_client: AsyncKernel) -> None: + deployment = await async_client.apps.deployments.create( + entrypoint_rel_path="src/app.py", + file=b"raw file contents", + ) + assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: + deployment = await async_client.apps.deployments.create( + entrypoint_rel_path="src/app.py", + file=b"raw file contents", + force=False, + region="aws.us-east-1a", + version="1.0.0", + ) + assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_raw_response_create(self, async_client: AsyncKernel) -> None: + response = await async_client.apps.deployments.with_raw_response.create( + entrypoint_rel_path="src/app.py", + file=b"raw file contents", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + deployment = await response.parse() + assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: + async with async_client.apps.deployments.with_streaming_response.create( + entrypoint_rel_path="src/app.py", + file=b"raw file contents", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + deployment = await response.parse() + assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/apps/test_invocations.py b/tests/api_resources/apps/test_invocations.py new file mode 100644 index 00000000..61af0316 --- /dev/null +++ b/tests/api_resources/apps/test_invocations.py @@ -0,0 +1,208 @@ +# 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 kernel import Kernel, AsyncKernel +from tests.utils import assert_matches_type +from kernel.types.apps import InvocationCreateResponse, InvocationRetrieveResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestInvocations: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip() + @parametrize + def test_method_create(self, client: Kernel) -> None: + invocation = client.apps.invocations.create( + action_name="analyze", + app_name="my-app", + version="1.0.0", + ) + assert_matches_type(InvocationCreateResponse, invocation, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_method_create_with_all_params(self, client: Kernel) -> None: + invocation = client.apps.invocations.create( + action_name="analyze", + app_name="my-app", + version="1.0.0", + payload='{"data":"example input"}', + ) + assert_matches_type(InvocationCreateResponse, invocation, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_raw_response_create(self, client: Kernel) -> None: + response = client.apps.invocations.with_raw_response.create( + action_name="analyze", + app_name="my-app", + version="1.0.0", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + invocation = response.parse() + assert_matches_type(InvocationCreateResponse, invocation, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_streaming_response_create(self, client: Kernel) -> None: + with client.apps.invocations.with_streaming_response.create( + action_name="analyze", + app_name="my-app", + version="1.0.0", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + invocation = response.parse() + assert_matches_type(InvocationCreateResponse, invocation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + def test_method_retrieve(self, client: Kernel) -> None: + invocation = client.apps.invocations.retrieve( + "id", + ) + assert_matches_type(InvocationRetrieveResponse, invocation, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_raw_response_retrieve(self, client: Kernel) -> None: + response = client.apps.invocations.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + invocation = response.parse() + assert_matches_type(InvocationRetrieveResponse, invocation, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_streaming_response_retrieve(self, client: Kernel) -> None: + with client.apps.invocations.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + invocation = response.parse() + assert_matches_type(InvocationRetrieveResponse, invocation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + def test_path_params_retrieve(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.apps.invocations.with_raw_response.retrieve( + "", + ) + + +class TestAsyncInvocations: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip() + @parametrize + async def test_method_create(self, async_client: AsyncKernel) -> None: + invocation = await async_client.apps.invocations.create( + action_name="analyze", + app_name="my-app", + version="1.0.0", + ) + assert_matches_type(InvocationCreateResponse, invocation, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: + invocation = await async_client.apps.invocations.create( + action_name="analyze", + app_name="my-app", + version="1.0.0", + payload='{"data":"example input"}', + ) + assert_matches_type(InvocationCreateResponse, invocation, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_raw_response_create(self, async_client: AsyncKernel) -> None: + response = await async_client.apps.invocations.with_raw_response.create( + action_name="analyze", + app_name="my-app", + version="1.0.0", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + invocation = await response.parse() + assert_matches_type(InvocationCreateResponse, invocation, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: + async with async_client.apps.invocations.with_streaming_response.create( + action_name="analyze", + app_name="my-app", + version="1.0.0", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + invocation = await response.parse() + assert_matches_type(InvocationCreateResponse, invocation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + async def test_method_retrieve(self, async_client: AsyncKernel) -> None: + invocation = await async_client.apps.invocations.retrieve( + "id", + ) + assert_matches_type(InvocationRetrieveResponse, invocation, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: + response = await async_client.apps.invocations.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + invocation = await response.parse() + assert_matches_type(InvocationRetrieveResponse, invocation, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: + async with async_client.apps.invocations.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + invocation = await response.parse() + assert_matches_type(InvocationRetrieveResponse, invocation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.apps.invocations.with_raw_response.retrieve( + "", + ) diff --git a/tests/api_resources/test_apps.py b/tests/api_resources/test_apps.py deleted file mode 100644 index 87194867..00000000 --- a/tests/api_resources/test_apps.py +++ /dev/null @@ -1,294 +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 kernel import Kernel, AsyncKernel -from tests.utils import assert_matches_type -from kernel.types import ( - AppDeployResponse, - AppInvokeResponse, - AppRetrieveInvocationResponse, -) - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestApps: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip() - @parametrize - def test_method_deploy(self, client: Kernel) -> None: - app = client.apps.deploy( - entrypoint_rel_path="app.py", - file=b"raw file contents", - ) - assert_matches_type(AppDeployResponse, app, path=["response"]) - - @pytest.mark.skip() - @parametrize - def test_method_deploy_with_all_params(self, client: Kernel) -> None: - app = client.apps.deploy( - entrypoint_rel_path="app.py", - file=b"raw file contents", - force="false", - region="aws.us-east-1a", - version="1.0.0", - ) - assert_matches_type(AppDeployResponse, app, path=["response"]) - - @pytest.mark.skip() - @parametrize - def test_raw_response_deploy(self, client: Kernel) -> None: - response = client.apps.with_raw_response.deploy( - entrypoint_rel_path="app.py", - file=b"raw file contents", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - app = response.parse() - assert_matches_type(AppDeployResponse, app, path=["response"]) - - @pytest.mark.skip() - @parametrize - def test_streaming_response_deploy(self, client: Kernel) -> None: - with client.apps.with_streaming_response.deploy( - entrypoint_rel_path="app.py", - file=b"raw file contents", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - app = response.parse() - assert_matches_type(AppDeployResponse, app, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip() - @parametrize - def test_method_invoke(self, client: Kernel) -> None: - app = client.apps.invoke( - action_name="analyze", - app_name="my-awesome-app", - payload={"data": "example input"}, - version="1.0.0", - ) - assert_matches_type(AppInvokeResponse, app, path=["response"]) - - @pytest.mark.skip() - @parametrize - def test_raw_response_invoke(self, client: Kernel) -> None: - response = client.apps.with_raw_response.invoke( - action_name="analyze", - app_name="my-awesome-app", - payload={"data": "example input"}, - version="1.0.0", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - app = response.parse() - assert_matches_type(AppInvokeResponse, app, path=["response"]) - - @pytest.mark.skip() - @parametrize - def test_streaming_response_invoke(self, client: Kernel) -> None: - with client.apps.with_streaming_response.invoke( - action_name="analyze", - app_name="my-awesome-app", - payload={"data": "example input"}, - version="1.0.0", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - app = response.parse() - assert_matches_type(AppInvokeResponse, app, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip() - @parametrize - def test_method_retrieve_invocation(self, client: Kernel) -> None: - app = client.apps.retrieve_invocation( - "id", - ) - assert_matches_type(AppRetrieveInvocationResponse, app, path=["response"]) - - @pytest.mark.skip() - @parametrize - def test_raw_response_retrieve_invocation(self, client: Kernel) -> None: - response = client.apps.with_raw_response.retrieve_invocation( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - app = response.parse() - assert_matches_type(AppRetrieveInvocationResponse, app, path=["response"]) - - @pytest.mark.skip() - @parametrize - def test_streaming_response_retrieve_invocation(self, client: Kernel) -> None: - with client.apps.with_streaming_response.retrieve_invocation( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - app = response.parse() - assert_matches_type(AppRetrieveInvocationResponse, app, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip() - @parametrize - def test_path_params_retrieve_invocation(self, client: Kernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - client.apps.with_raw_response.retrieve_invocation( - "", - ) - - -class TestAsyncApps: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip() - @parametrize - async def test_method_deploy(self, async_client: AsyncKernel) -> None: - app = await async_client.apps.deploy( - entrypoint_rel_path="app.py", - file=b"raw file contents", - ) - assert_matches_type(AppDeployResponse, app, path=["response"]) - - @pytest.mark.skip() - @parametrize - async def test_method_deploy_with_all_params(self, async_client: AsyncKernel) -> None: - app = await async_client.apps.deploy( - entrypoint_rel_path="app.py", - file=b"raw file contents", - force="false", - region="aws.us-east-1a", - version="1.0.0", - ) - assert_matches_type(AppDeployResponse, app, path=["response"]) - - @pytest.mark.skip() - @parametrize - async def test_raw_response_deploy(self, async_client: AsyncKernel) -> None: - response = await async_client.apps.with_raw_response.deploy( - entrypoint_rel_path="app.py", - file=b"raw file contents", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - app = await response.parse() - assert_matches_type(AppDeployResponse, app, path=["response"]) - - @pytest.mark.skip() - @parametrize - async def test_streaming_response_deploy(self, async_client: AsyncKernel) -> None: - async with async_client.apps.with_streaming_response.deploy( - entrypoint_rel_path="app.py", - file=b"raw file contents", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - app = await response.parse() - assert_matches_type(AppDeployResponse, app, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip() - @parametrize - async def test_method_invoke(self, async_client: AsyncKernel) -> None: - app = await async_client.apps.invoke( - action_name="analyze", - app_name="my-awesome-app", - payload={"data": "example input"}, - version="1.0.0", - ) - assert_matches_type(AppInvokeResponse, app, path=["response"]) - - @pytest.mark.skip() - @parametrize - async def test_raw_response_invoke(self, async_client: AsyncKernel) -> None: - response = await async_client.apps.with_raw_response.invoke( - action_name="analyze", - app_name="my-awesome-app", - payload={"data": "example input"}, - version="1.0.0", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - app = await response.parse() - assert_matches_type(AppInvokeResponse, app, path=["response"]) - - @pytest.mark.skip() - @parametrize - async def test_streaming_response_invoke(self, async_client: AsyncKernel) -> None: - async with async_client.apps.with_streaming_response.invoke( - action_name="analyze", - app_name="my-awesome-app", - payload={"data": "example input"}, - version="1.0.0", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - app = await response.parse() - assert_matches_type(AppInvokeResponse, app, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip() - @parametrize - async def test_method_retrieve_invocation(self, async_client: AsyncKernel) -> None: - app = await async_client.apps.retrieve_invocation( - "id", - ) - assert_matches_type(AppRetrieveInvocationResponse, app, path=["response"]) - - @pytest.mark.skip() - @parametrize - async def test_raw_response_retrieve_invocation(self, async_client: AsyncKernel) -> None: - response = await async_client.apps.with_raw_response.retrieve_invocation( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - app = await response.parse() - assert_matches_type(AppRetrieveInvocationResponse, app, path=["response"]) - - @pytest.mark.skip() - @parametrize - async def test_streaming_response_retrieve_invocation(self, async_client: AsyncKernel) -> None: - async with async_client.apps.with_streaming_response.retrieve_invocation( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - app = await response.parse() - assert_matches_type(AppRetrieveInvocationResponse, app, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip() - @parametrize - async def test_path_params_retrieve_invocation(self, async_client: AsyncKernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - await async_client.apps.with_raw_response.retrieve_invocation( - "", - ) diff --git a/tests/api_resources/test_browser.py b/tests/api_resources/test_browser.py deleted file mode 100644 index 3280e05b..00000000 --- a/tests/api_resources/test_browser.py +++ /dev/null @@ -1,90 +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 kernel import Kernel, AsyncKernel -from tests.utils import assert_matches_type -from kernel.types import BrowserCreateSessionResponse - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestBrowser: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip() - @parametrize - def test_method_create_session(self, client: Kernel) -> None: - browser = client.browser.create_session( - invocation_id="invocationId", - ) - assert_matches_type(BrowserCreateSessionResponse, browser, path=["response"]) - - @pytest.mark.skip() - @parametrize - def test_raw_response_create_session(self, client: Kernel) -> None: - response = client.browser.with_raw_response.create_session( - invocation_id="invocationId", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - browser = response.parse() - assert_matches_type(BrowserCreateSessionResponse, browser, path=["response"]) - - @pytest.mark.skip() - @parametrize - def test_streaming_response_create_session(self, client: Kernel) -> None: - with client.browser.with_streaming_response.create_session( - invocation_id="invocationId", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - browser = response.parse() - assert_matches_type(BrowserCreateSessionResponse, browser, path=["response"]) - - assert cast(Any, response.is_closed) is True - - -class TestAsyncBrowser: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip() - @parametrize - async def test_method_create_session(self, async_client: AsyncKernel) -> None: - browser = await async_client.browser.create_session( - invocation_id="invocationId", - ) - assert_matches_type(BrowserCreateSessionResponse, browser, path=["response"]) - - @pytest.mark.skip() - @parametrize - async def test_raw_response_create_session(self, async_client: AsyncKernel) -> None: - response = await async_client.browser.with_raw_response.create_session( - invocation_id="invocationId", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - browser = await response.parse() - assert_matches_type(BrowserCreateSessionResponse, browser, path=["response"]) - - @pytest.mark.skip() - @parametrize - async def test_streaming_response_create_session(self, async_client: AsyncKernel) -> None: - async with async_client.browser.with_streaming_response.create_session( - invocation_id="invocationId", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - browser = await response.parse() - assert_matches_type(BrowserCreateSessionResponse, browser, path=["response"]) - - assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py new file mode 100644 index 00000000..91fc83ec --- /dev/null +++ b/tests/api_resources/test_browsers.py @@ -0,0 +1,174 @@ +# 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 kernel import Kernel, AsyncKernel +from tests.utils import assert_matches_type +from kernel.types import BrowserCreateResponse, BrowserRetrieveResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestBrowsers: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip() + @parametrize + def test_method_create(self, client: Kernel) -> None: + browser = client.browsers.create( + invocation_id="ckqwer3o20000jb9s7abcdef", + ) + assert_matches_type(BrowserCreateResponse, browser, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_raw_response_create(self, client: Kernel) -> None: + response = client.browsers.with_raw_response.create( + invocation_id="ckqwer3o20000jb9s7abcdef", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser = response.parse() + assert_matches_type(BrowserCreateResponse, browser, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_streaming_response_create(self, client: Kernel) -> None: + with client.browsers.with_streaming_response.create( + invocation_id="ckqwer3o20000jb9s7abcdef", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser = response.parse() + assert_matches_type(BrowserCreateResponse, browser, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + def test_method_retrieve(self, client: Kernel) -> None: + browser = client.browsers.retrieve( + "id", + ) + assert_matches_type(BrowserRetrieveResponse, browser, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_raw_response_retrieve(self, client: Kernel) -> None: + response = client.browsers.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser = response.parse() + assert_matches_type(BrowserRetrieveResponse, browser, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_streaming_response_retrieve(self, client: Kernel) -> None: + with client.browsers.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser = response.parse() + assert_matches_type(BrowserRetrieveResponse, browser, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + def test_path_params_retrieve(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.with_raw_response.retrieve( + "", + ) + + +class TestAsyncBrowsers: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip() + @parametrize + async def test_method_create(self, async_client: AsyncKernel) -> None: + browser = await async_client.browsers.create( + invocation_id="ckqwer3o20000jb9s7abcdef", + ) + assert_matches_type(BrowserCreateResponse, browser, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_raw_response_create(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.with_raw_response.create( + invocation_id="ckqwer3o20000jb9s7abcdef", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser = await response.parse() + assert_matches_type(BrowserCreateResponse, browser, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.with_streaming_response.create( + invocation_id="ckqwer3o20000jb9s7abcdef", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser = await response.parse() + assert_matches_type(BrowserCreateResponse, browser, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + async def test_method_retrieve(self, async_client: AsyncKernel) -> None: + browser = await async_client.browsers.retrieve( + "id", + ) + assert_matches_type(BrowserRetrieveResponse, browser, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser = await response.parse() + assert_matches_type(BrowserRetrieveResponse, browser, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser = await response.parse() + assert_matches_type(BrowserRetrieveResponse, browser, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.with_raw_response.retrieve( + "", + ) diff --git a/tests/test_client.py b/tests/test_client.py index 713ce3c1..00d7a014 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -28,7 +28,7 @@ from kernel._constants import RAW_RESPONSE_HEADER from kernel._exceptions import KernelError, APIStatusError, APITimeoutError, APIResponseValidationError from kernel._base_client import DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, BaseClient, make_request_options -from kernel.types.app_deploy_params import AppDeployParams +from kernel.types.browser_create_params import BrowserCreateParams from .utils import update_env @@ -715,17 +715,12 @@ def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str @mock.patch("kernel._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: - respx_mock.post("/apps/deploy").mock(side_effect=httpx.TimeoutException("Test timeout error")) + respx_mock.post("/browsers").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): self.client.post( - "/apps/deploy", - body=cast( - object, - maybe_transform( - dict(entrypoint_rel_path="app.py", file=b"REPLACE_ME", version="REPLACE_ME"), AppDeployParams - ), - ), + "/browsers", + body=cast(object, maybe_transform(dict(invocation_id="REPLACE_ME"), BrowserCreateParams)), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -735,17 +730,12 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No @mock.patch("kernel._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: - respx_mock.post("/apps/deploy").mock(return_value=httpx.Response(500)) + respx_mock.post("/browsers").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): self.client.post( - "/apps/deploy", - body=cast( - object, - maybe_transform( - dict(entrypoint_rel_path="app.py", file=b"REPLACE_ME", version="REPLACE_ME"), AppDeployParams - ), - ), + "/browsers", + body=cast(object, maybe_transform(dict(invocation_id="REPLACE_ME"), BrowserCreateParams)), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -776,9 +766,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/apps/deploy").mock(side_effect=retry_handler) + respx_mock.post("/browsers").mock(side_effect=retry_handler) - response = client.apps.with_raw_response.deploy(entrypoint_rel_path="app.py", file=b"raw file contents") + response = client.browsers.with_raw_response.create(invocation_id="ckqwer3o20000jb9s7abcdef") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -800,10 +790,10 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/apps/deploy").mock(side_effect=retry_handler) + respx_mock.post("/browsers").mock(side_effect=retry_handler) - response = client.apps.with_raw_response.deploy( - entrypoint_rel_path="app.py", file=b"raw file contents", extra_headers={"x-stainless-retry-count": Omit()} + response = client.browsers.with_raw_response.create( + invocation_id="ckqwer3o20000jb9s7abcdef", extra_headers={"x-stainless-retry-count": Omit()} ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -825,10 +815,10 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/apps/deploy").mock(side_effect=retry_handler) + respx_mock.post("/browsers").mock(side_effect=retry_handler) - response = client.apps.with_raw_response.deploy( - entrypoint_rel_path="app.py", file=b"raw file contents", extra_headers={"x-stainless-retry-count": "42"} + response = client.browsers.with_raw_response.create( + invocation_id="ckqwer3o20000jb9s7abcdef", extra_headers={"x-stainless-retry-count": "42"} ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1515,17 +1505,12 @@ async def test_parse_retry_after_header(self, remaining_retries: int, retry_afte @mock.patch("kernel._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: - respx_mock.post("/apps/deploy").mock(side_effect=httpx.TimeoutException("Test timeout error")) + respx_mock.post("/browsers").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): await self.client.post( - "/apps/deploy", - body=cast( - object, - maybe_transform( - dict(entrypoint_rel_path="app.py", file=b"REPLACE_ME", version="REPLACE_ME"), AppDeployParams - ), - ), + "/browsers", + body=cast(object, maybe_transform(dict(invocation_id="REPLACE_ME"), BrowserCreateParams)), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -1535,17 +1520,12 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) @mock.patch("kernel._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: - respx_mock.post("/apps/deploy").mock(return_value=httpx.Response(500)) + respx_mock.post("/browsers").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): await self.client.post( - "/apps/deploy", - body=cast( - object, - maybe_transform( - dict(entrypoint_rel_path="app.py", file=b"REPLACE_ME", version="REPLACE_ME"), AppDeployParams - ), - ), + "/browsers", + body=cast(object, maybe_transform(dict(invocation_id="REPLACE_ME"), BrowserCreateParams)), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -1577,9 +1557,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/apps/deploy").mock(side_effect=retry_handler) + respx_mock.post("/browsers").mock(side_effect=retry_handler) - response = await client.apps.with_raw_response.deploy(entrypoint_rel_path="app.py", file=b"raw file contents") + response = await client.browsers.with_raw_response.create(invocation_id="ckqwer3o20000jb9s7abcdef") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1602,10 +1582,10 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/apps/deploy").mock(side_effect=retry_handler) + respx_mock.post("/browsers").mock(side_effect=retry_handler) - response = await client.apps.with_raw_response.deploy( - entrypoint_rel_path="app.py", file=b"raw file contents", extra_headers={"x-stainless-retry-count": Omit()} + response = await client.browsers.with_raw_response.create( + invocation_id="ckqwer3o20000jb9s7abcdef", extra_headers={"x-stainless-retry-count": Omit()} ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -1628,10 +1608,10 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/apps/deploy").mock(side_effect=retry_handler) + respx_mock.post("/browsers").mock(side_effect=retry_handler) - response = await client.apps.with_raw_response.deploy( - entrypoint_rel_path="app.py", file=b"raw file contents", extra_headers={"x-stainless-retry-count": "42"} + response = await client.browsers.with_raw_response.create( + invocation_id="ckqwer3o20000jb9s7abcdef", extra_headers={"x-stainless-retry-count": "42"} ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" From 91b04e8da22ee0cee55238c682227c05ada46d0d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 18:54:43 +0000 Subject: [PATCH 032/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 46b9b6b2..3b005e52 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.9" + ".": "0.1.0-alpha.10" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 5045418e..b8cbab88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.1.0-alpha.9" +version = "0.1.0-alpha.10" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 7716ecb9..edb86b3e 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.1.0-alpha.9" # x-release-please-version +__version__ = "0.1.0-alpha.10" # x-release-please-version From cc7a21be94dd86c920893f2544e48ca3cfaca425 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 19:44:13 +0000 Subject: [PATCH 033/448] feat(api): update via SDK Studio --- .stats.yml | 4 ++-- src/kernel/resources/apps/deployments.py | 12 +++++++++++- src/kernel/types/apps/deployment_create_params.py | 7 +++++++ tests/api_resources/apps/test_deployments.py | 2 ++ 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 6f21bcbb..24b7a04f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 5 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-f40e779e2a48f5e37361f2f4a9879e5c40f2851b8033c23db69ec7b91242bf69.yml -openapi_spec_hash: 2dfa146149e61363f1ec40bf9251eb7c +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-1fe396b957ced73281fc0a61a69b630836aa5c89a8dccce2c5a1716bc9775e80.yml +openapi_spec_hash: 9a0d67fb0781be034b77839584109638 config_hash: 2ddaa85513b6670889b1a56c905423c7 diff --git a/src/kernel/resources/apps/deployments.py b/src/kernel/resources/apps/deployments.py index 0b88fd1e..89f7992a 100644 --- a/src/kernel/resources/apps/deployments.py +++ b/src/kernel/resources/apps/deployments.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Mapping, cast +from typing import Dict, Mapping, cast from typing_extensions import Literal import httpx @@ -49,6 +49,7 @@ def create( *, entrypoint_rel_path: str, file: FileTypes, + env_vars: Dict[str, str] | NotGiven = NOT_GIVEN, force: bool | NotGiven = NOT_GIVEN, region: Literal["aws.us-east-1a"] | NotGiven = NOT_GIVEN, version: str | NotGiven = NOT_GIVEN, @@ -67,6 +68,9 @@ def create( file: ZIP file containing the application source directory + env_vars: Map of environment variables to set for the deployed application. Each key-value + pair represents an environment variable. + force: Allow overwriting an existing app version region: Region for deployment. Currently we only support "aws.us-east-1a" @@ -85,6 +89,7 @@ def create( { "entrypoint_rel_path": entrypoint_rel_path, "file": file, + "env_vars": env_vars, "force": force, "region": region, "version": version, @@ -131,6 +136,7 @@ async def create( *, entrypoint_rel_path: str, file: FileTypes, + env_vars: Dict[str, str] | NotGiven = NOT_GIVEN, force: bool | NotGiven = NOT_GIVEN, region: Literal["aws.us-east-1a"] | NotGiven = NOT_GIVEN, version: str | NotGiven = NOT_GIVEN, @@ -149,6 +155,9 @@ async def create( file: ZIP file containing the application source directory + env_vars: Map of environment variables to set for the deployed application. Each key-value + pair represents an environment variable. + force: Allow overwriting an existing app version region: Region for deployment. Currently we only support "aws.us-east-1a" @@ -167,6 +176,7 @@ async def create( { "entrypoint_rel_path": entrypoint_rel_path, "file": file, + "env_vars": env_vars, "force": force, "region": region, "version": version, diff --git a/src/kernel/types/apps/deployment_create_params.py b/src/kernel/types/apps/deployment_create_params.py index 92ff2586..cd1a7b53 100644 --- a/src/kernel/types/apps/deployment_create_params.py +++ b/src/kernel/types/apps/deployment_create_params.py @@ -2,6 +2,7 @@ from __future__ import annotations +from typing import Dict from typing_extensions import Literal, Required, TypedDict from ..._types import FileTypes @@ -16,6 +17,12 @@ class DeploymentCreateParams(TypedDict, total=False): file: Required[FileTypes] """ZIP file containing the application source directory""" + env_vars: Dict[str, str] + """Map of environment variables to set for the deployed application. + + Each key-value pair represents an environment variable. + """ + force: bool """Allow overwriting an existing app version""" diff --git a/tests/api_resources/apps/test_deployments.py b/tests/api_resources/apps/test_deployments.py index e2f2a3dd..3b4ea03e 100644 --- a/tests/api_resources/apps/test_deployments.py +++ b/tests/api_resources/apps/test_deployments.py @@ -32,6 +32,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: deployment = client.apps.deployments.create( entrypoint_rel_path="src/app.py", file=b"raw file contents", + env_vars={"foo": "string"}, force=False, region="aws.us-east-1a", version="1.0.0", @@ -85,6 +86,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> deployment = await async_client.apps.deployments.create( entrypoint_rel_path="src/app.py", file=b"raw file contents", + env_vars={"foo": "string"}, force=False, region="aws.us-east-1a", version="1.0.0", From bdb2d479af3288b56a505ae8db47a35acb0016e3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 19:47:35 +0000 Subject: [PATCH 034/448] feat(api): update via SDK Studio --- .stats.yml | 2 +- README.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 24b7a04f..4dfbf428 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 5 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-1fe396b957ced73281fc0a61a69b630836aa5c89a8dccce2c5a1716bc9775e80.yml openapi_spec_hash: 9a0d67fb0781be034b77839584109638 -config_hash: 2ddaa85513b6670889b1a56c905423c7 +config_hash: df889df131f7438197abd59faace3c77 diff --git a/README.md b/README.md index 1f5a5bb6..b9a0ae80 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ client = Kernel( deployment = client.apps.deployments.create( entrypoint_rel_path="main.ts", file=b"REPLACE_ME", + env_vars={"OPENAI_API_KEY": "x"}, version="1.0.0", ) print(deployment.apps) @@ -66,6 +67,7 @@ async def main() -> None: deployment = await client.apps.deployments.create( entrypoint_rel_path="main.ts", file=b"REPLACE_ME", + env_vars={"OPENAI_API_KEY": "x"}, version="1.0.0", ) print(deployment.apps) From a3eddb871d1e67cf69d5c3564ef61331f56fbe29 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 19:48:53 +0000 Subject: [PATCH 035/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 3b005e52..ee49ac2d 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.10" + ".": "0.1.0-alpha.11" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index b8cbab88..7f1d0e7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.1.0-alpha.10" +version = "0.1.0-alpha.11" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index edb86b3e..ec24ea7c 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.1.0-alpha.10" # x-release-please-version +__version__ = "0.1.0-alpha.11" # x-release-please-version From 6b367d711d801509fc6b6893a0386f4df30e6737 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 20:44:38 +0000 Subject: [PATCH 036/448] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 4dfbf428..6d03c395 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 5 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-1fe396b957ced73281fc0a61a69b630836aa5c89a8dccce2c5a1716bc9775e80.yml -openapi_spec_hash: 9a0d67fb0781be034b77839584109638 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-3b19f5e2b96ede3193aa7a24d3f82d1406b8a16ea25e98ba3956e4a1a2376ad7.yml +openapi_spec_hash: b62a6e73ddcec71674973f795a5790ac config_hash: df889df131f7438197abd59faace3c77 From b583812d04455394a5bb9e003a9d7e83e9a8df6c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 20:48:02 +0000 Subject: [PATCH 037/448] feat(api): update via SDK Studio --- .stats.yml | 8 +- api.md | 3 +- src/kernel/resources/apps/deployments.py | 90 +++++++++++++++++ src/kernel/types/apps/__init__.py | 1 + .../types/apps/deployment_follow_response.py | 63 ++++++++++++ tests/api_resources/apps/test_deployments.py | 98 +++++++++++++++++++ 6 files changed, 258 insertions(+), 5 deletions(-) create mode 100644 src/kernel/types/apps/deployment_follow_response.py diff --git a/.stats.yml b/.stats.yml index 6d03c395..01f342cf 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 5 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-3b19f5e2b96ede3193aa7a24d3f82d1406b8a16ea25e98ba3956e4a1a2376ad7.yml -openapi_spec_hash: b62a6e73ddcec71674973f795a5790ac -config_hash: df889df131f7438197abd59faace3c77 +configured_endpoints: 6 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-19b0d17ba368f32827ee322d15a7f4ff7e1f3bbf66606fad227b3465f8ffc5ab.yml +openapi_spec_hash: 4a3cb766898e8a134ef99fe6c4c87736 +config_hash: 9018b7ff17f8de1bc3e99a0ae2f2df68 diff --git a/api.md b/api.md index 3456b565..32a9bb03 100644 --- a/api.md +++ b/api.md @@ -5,12 +5,13 @@ Types: ```python -from kernel.types.apps import DeploymentCreateResponse +from kernel.types.apps import DeploymentCreateResponse, DeploymentFollowResponse ``` Methods: - client.apps.deployments.create(\*\*params) -> DeploymentCreateResponse +- client.apps.deployments.follow(id) -> DeploymentFollowResponse ## Invocations diff --git a/src/kernel/resources/apps/deployments.py b/src/kernel/resources/apps/deployments.py index 89f7992a..9a1cecaf 100644 --- a/src/kernel/resources/apps/deployments.py +++ b/src/kernel/resources/apps/deployments.py @@ -17,9 +17,11 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) +from ..._streaming import Stream, AsyncStream from ...types.apps import deployment_create_params from ..._base_client import make_request_options from ...types.apps.deployment_create_response import DeploymentCreateResponse +from ...types.apps.deployment_follow_response import DeploymentFollowResponse __all__ = ["DeploymentsResource", "AsyncDeploymentsResource"] @@ -110,6 +112,44 @@ def create( cast_to=DeploymentCreateResponse, ) + def follow( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Stream[DeploymentFollowResponse]: + """ + Establishes a Server-Sent Events (SSE) stream that delivers real-time logs and + status updates for a deployed application. The stream terminates automatically + once the application reaches a terminal state. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} + return self._get( + f"/apps/{id}/events", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=DeploymentFollowResponse, + stream=True, + stream_cls=Stream[DeploymentFollowResponse], + ) + class AsyncDeploymentsResource(AsyncAPIResource): @cached_property @@ -197,6 +237,44 @@ async def create( cast_to=DeploymentCreateResponse, ) + async def follow( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncStream[DeploymentFollowResponse]: + """ + Establishes a Server-Sent Events (SSE) stream that delivers real-time logs and + status updates for a deployed application. The stream terminates automatically + once the application reaches a terminal state. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} + return await self._get( + f"/apps/{id}/events", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=DeploymentFollowResponse, + stream=True, + stream_cls=AsyncStream[DeploymentFollowResponse], + ) + class DeploymentsResourceWithRawResponse: def __init__(self, deployments: DeploymentsResource) -> None: @@ -205,6 +283,9 @@ def __init__(self, deployments: DeploymentsResource) -> None: self.create = to_raw_response_wrapper( deployments.create, ) + self.follow = to_raw_response_wrapper( + deployments.follow, + ) class AsyncDeploymentsResourceWithRawResponse: @@ -214,6 +295,9 @@ def __init__(self, deployments: AsyncDeploymentsResource) -> None: self.create = async_to_raw_response_wrapper( deployments.create, ) + self.follow = async_to_raw_response_wrapper( + deployments.follow, + ) class DeploymentsResourceWithStreamingResponse: @@ -223,6 +307,9 @@ def __init__(self, deployments: DeploymentsResource) -> None: self.create = to_streamed_response_wrapper( deployments.create, ) + self.follow = to_streamed_response_wrapper( + deployments.follow, + ) class AsyncDeploymentsResourceWithStreamingResponse: @@ -232,3 +319,6 @@ def __init__(self, deployments: AsyncDeploymentsResource) -> None: self.create = async_to_streamed_response_wrapper( deployments.create, ) + self.follow = async_to_streamed_response_wrapper( + deployments.follow, + ) diff --git a/src/kernel/types/apps/__init__.py b/src/kernel/types/apps/__init__.py index f4d451cf..425fffd4 100644 --- a/src/kernel/types/apps/__init__.py +++ b/src/kernel/types/apps/__init__.py @@ -5,5 +5,6 @@ from .deployment_create_params import DeploymentCreateParams as DeploymentCreateParams from .invocation_create_params import InvocationCreateParams as InvocationCreateParams from .deployment_create_response import DeploymentCreateResponse as DeploymentCreateResponse +from .deployment_follow_response import DeploymentFollowResponse as DeploymentFollowResponse from .invocation_create_response import InvocationCreateResponse as InvocationCreateResponse from .invocation_retrieve_response import InvocationRetrieveResponse as InvocationRetrieveResponse diff --git a/src/kernel/types/apps/deployment_follow_response.py b/src/kernel/types/apps/deployment_follow_response.py new file mode 100644 index 00000000..4374485b --- /dev/null +++ b/src/kernel/types/apps/deployment_follow_response.py @@ -0,0 +1,63 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Union, Optional +from datetime import datetime +from typing_extensions import Literal, Annotated, TypeAlias + +from ..._utils import PropertyInfo +from ..._models import BaseModel + +__all__ = [ + "DeploymentFollowResponse", + "DeploymentFollowResponseItem", + "DeploymentFollowResponseItemStateEvent", + "DeploymentFollowResponseItemStateUpdateEvent", + "DeploymentFollowResponseItemLogEvent", +] + + +class DeploymentFollowResponseItemStateEvent(BaseModel): + event: Literal["state"] + """Event type identifier (always "state").""" + + state: str + """ + Current application state (e.g., "deploying", "running", "succeeded", "failed"). + """ + + timestamp: Optional[datetime] = None + """Time the state was reported.""" + + +class DeploymentFollowResponseItemStateUpdateEvent(BaseModel): + event: Literal["state_update"] + """Event type identifier (always "state_update").""" + + state: str + """New application state (e.g., "running", "succeeded", "failed").""" + + timestamp: Optional[datetime] = None + """Time the state change occurred.""" + + +class DeploymentFollowResponseItemLogEvent(BaseModel): + event: Literal["log"] + """Event type identifier (always "log").""" + + message: str + """Log message text.""" + + timestamp: Optional[datetime] = None + """Time the log entry was produced.""" + + +DeploymentFollowResponseItem: TypeAlias = Annotated[ + Union[ + DeploymentFollowResponseItemStateEvent, + DeploymentFollowResponseItemStateUpdateEvent, + DeploymentFollowResponseItemLogEvent, + ], + PropertyInfo(discriminator="event"), +] + +DeploymentFollowResponse: TypeAlias = List[DeploymentFollowResponseItem] diff --git a/tests/api_resources/apps/test_deployments.py b/tests/api_resources/apps/test_deployments.py index 3b4ea03e..0a93c44c 100644 --- a/tests/api_resources/apps/test_deployments.py +++ b/tests/api_resources/apps/test_deployments.py @@ -67,6 +67,55 @@ def test_streaming_response_create(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + def test_method_follow(self, client: Kernel) -> None: + deployment_stream = client.apps.deployments.follow( + "id", + ) + deployment_stream.response.close() + + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + def test_raw_response_follow(self, client: Kernel) -> None: + response = client.apps.deployments.with_raw_response.follow( + "id", + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = response.parse() + stream.close() + + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + def test_streaming_response_follow(self, client: Kernel) -> None: + with client.apps.deployments.with_streaming_response.follow( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = response.parse() + stream.close() + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + def test_path_params_follow(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.apps.deployments.with_raw_response.follow( + "", + ) + class TestAsyncDeployments: parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) @@ -120,3 +169,52 @@ async def test_streaming_response_create(self, async_client: AsyncKernel) -> Non assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) assert cast(Any, response.is_closed) is True + + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + async def test_method_follow(self, async_client: AsyncKernel) -> None: + deployment_stream = await async_client.apps.deployments.follow( + "id", + ) + await deployment_stream.response.aclose() + + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + async def test_raw_response_follow(self, async_client: AsyncKernel) -> None: + response = await async_client.apps.deployments.with_raw_response.follow( + "id", + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = await response.parse() + await stream.close() + + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + async def test_streaming_response_follow(self, async_client: AsyncKernel) -> None: + async with async_client.apps.deployments.with_streaming_response.follow( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = await response.parse() + await stream.close() + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + async def test_path_params_follow(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.apps.deployments.with_raw_response.follow( + "", + ) From 5e415a887b41df5c77d6d72d52b33d00421a7a51 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 21:18:30 +0000 Subject: [PATCH 038/448] feat(api): update via SDK Studio --- .stats.yml | 8 +- api.md | 10 +++ src/kernel/resources/apps/apps.py | 125 ++++++++++++++++++++++++++ src/kernel/types/__init__.py | 2 + src/kernel/types/app_list_params.py | 15 ++++ src/kernel/types/app_list_response.py | 28 ++++++ tests/api_resources/test_apps.py | 96 ++++++++++++++++++++ 7 files changed, 280 insertions(+), 4 deletions(-) create mode 100644 src/kernel/types/app_list_params.py create mode 100644 src/kernel/types/app_list_response.py create mode 100644 tests/api_resources/test_apps.py diff --git a/.stats.yml b/.stats.yml index 01f342cf..01c41add 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 6 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-19b0d17ba368f32827ee322d15a7f4ff7e1f3bbf66606fad227b3465f8ffc5ab.yml -openapi_spec_hash: 4a3cb766898e8a134ef99fe6c4c87736 -config_hash: 9018b7ff17f8de1bc3e99a0ae2f2df68 +configured_endpoints: 7 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-c9d64df733f286f09d2203f4e3d820ce57e8d4c629c5e2db4e2bfac91fbc1598.yml +openapi_spec_hash: fa407611fc566d55f403864fbfaa6c23 +config_hash: 7f67c5b95af1e4b39525515240b72275 diff --git a/api.md b/api.md index 32a9bb03..63d7b00d 100644 --- a/api.md +++ b/api.md @@ -1,5 +1,15 @@ # Apps +Types: + +```python +from kernel.types import AppListResponse +``` + +Methods: + +- client.apps.list(\*\*params) -> AppListResponse + ## Deployments Types: diff --git a/src/kernel/resources/apps/apps.py b/src/kernel/resources/apps/apps.py index 848b765b..9a5f6670 100644 --- a/src/kernel/resources/apps/apps.py +++ b/src/kernel/resources/apps/apps.py @@ -2,8 +2,19 @@ from __future__ import annotations +import httpx + +from ...types import app_list_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 .deployments import ( DeploymentsResource, AsyncDeploymentsResource, @@ -20,6 +31,8 @@ InvocationsResourceWithStreamingResponse, AsyncInvocationsResourceWithStreamingResponse, ) +from ..._base_client import make_request_options +from ...types.app_list_response import AppListResponse __all__ = ["AppsResource", "AsyncAppsResource"] @@ -52,6 +65,54 @@ def with_streaming_response(self) -> AppsResourceWithStreamingResponse: """ return AppsResourceWithStreamingResponse(self) + def list( + self, + *, + app_name: str | NotGiven = NOT_GIVEN, + version: 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, + ) -> AppListResponse: + """List application versions for the authenticated user. + + Optionally filter by app + name and/or version label. + + Args: + app_name: Filter results by application name. + + version: Filter results by version label. + + 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( + "/apps", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "app_name": app_name, + "version": version, + }, + app_list_params.AppListParams, + ), + ), + cast_to=AppListResponse, + ) + class AsyncAppsResource(AsyncAPIResource): @cached_property @@ -81,11 +142,63 @@ def with_streaming_response(self) -> AsyncAppsResourceWithStreamingResponse: """ return AsyncAppsResourceWithStreamingResponse(self) + async def list( + self, + *, + app_name: str | NotGiven = NOT_GIVEN, + version: 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, + ) -> AppListResponse: + """List application versions for the authenticated user. + + Optionally filter by app + name and/or version label. + + Args: + app_name: Filter results by application name. + + version: Filter results by version label. + + 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( + "/apps", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "app_name": app_name, + "version": version, + }, + app_list_params.AppListParams, + ), + ), + cast_to=AppListResponse, + ) + class AppsResourceWithRawResponse: def __init__(self, apps: AppsResource) -> None: self._apps = apps + self.list = to_raw_response_wrapper( + apps.list, + ) + @cached_property def deployments(self) -> DeploymentsResourceWithRawResponse: return DeploymentsResourceWithRawResponse(self._apps.deployments) @@ -99,6 +212,10 @@ class AsyncAppsResourceWithRawResponse: def __init__(self, apps: AsyncAppsResource) -> None: self._apps = apps + self.list = async_to_raw_response_wrapper( + apps.list, + ) + @cached_property def deployments(self) -> AsyncDeploymentsResourceWithRawResponse: return AsyncDeploymentsResourceWithRawResponse(self._apps.deployments) @@ -112,6 +229,10 @@ class AppsResourceWithStreamingResponse: def __init__(self, apps: AppsResource) -> None: self._apps = apps + self.list = to_streamed_response_wrapper( + apps.list, + ) + @cached_property def deployments(self) -> DeploymentsResourceWithStreamingResponse: return DeploymentsResourceWithStreamingResponse(self._apps.deployments) @@ -125,6 +246,10 @@ class AsyncAppsResourceWithStreamingResponse: def __init__(self, apps: AsyncAppsResource) -> None: self._apps = apps + self.list = async_to_streamed_response_wrapper( + apps.list, + ) + @cached_property def deployments(self) -> AsyncDeploymentsResourceWithStreamingResponse: return AsyncDeploymentsResourceWithStreamingResponse(self._apps.deployments) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 282c8899..e7c3cecd 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -2,6 +2,8 @@ from __future__ import annotations +from .app_list_params import AppListParams as AppListParams +from .app_list_response import AppListResponse as AppListResponse from .browser_create_params import BrowserCreateParams as BrowserCreateParams from .browser_create_response import BrowserCreateResponse as BrowserCreateResponse from .browser_retrieve_response import BrowserRetrieveResponse as BrowserRetrieveResponse diff --git a/src/kernel/types/app_list_params.py b/src/kernel/types/app_list_params.py new file mode 100644 index 00000000..d4506a3e --- /dev/null +++ b/src/kernel/types/app_list_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["AppListParams"] + + +class AppListParams(TypedDict, total=False): + app_name: str + """Filter results by application name.""" + + version: str + """Filter results by version label.""" diff --git a/src/kernel/types/app_list_response.py b/src/kernel/types/app_list_response.py new file mode 100644 index 00000000..8a6f6218 --- /dev/null +++ b/src/kernel/types/app_list_response.py @@ -0,0 +1,28 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, List, Optional +from typing_extensions import TypeAlias + +from .._models import BaseModel + +__all__ = ["AppListResponse", "AppListResponseItem"] + + +class AppListResponseItem(BaseModel): + id: str + """Unique identifier for the app version""" + + app_name: str + """Name of the application""" + + region: str + """Deployment region code""" + + version: str + """Version label for the application""" + + env_vars: Optional[Dict[str, str]] = None + """Environment variables configured for this app version""" + + +AppListResponse: TypeAlias = List[AppListResponseItem] diff --git a/tests/api_resources/test_apps.py b/tests/api_resources/test_apps.py new file mode 100644 index 00000000..b902576a --- /dev/null +++ b/tests/api_resources/test_apps.py @@ -0,0 +1,96 @@ +# 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 kernel import Kernel, AsyncKernel +from tests.utils import assert_matches_type +from kernel.types import AppListResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestApps: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip() + @parametrize + def test_method_list(self, client: Kernel) -> None: + app = client.apps.list() + assert_matches_type(AppListResponse, app, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_method_list_with_all_params(self, client: Kernel) -> None: + app = client.apps.list( + app_name="app_name", + version="version", + ) + assert_matches_type(AppListResponse, app, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_raw_response_list(self, client: Kernel) -> None: + response = client.apps.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + app = response.parse() + assert_matches_type(AppListResponse, app, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_streaming_response_list(self, client: Kernel) -> None: + with client.apps.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + app = response.parse() + assert_matches_type(AppListResponse, app, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncApps: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip() + @parametrize + async def test_method_list(self, async_client: AsyncKernel) -> None: + app = await async_client.apps.list() + assert_matches_type(AppListResponse, app, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: + app = await async_client.apps.list( + app_name="app_name", + version="version", + ) + assert_matches_type(AppListResponse, app, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_raw_response_list(self, async_client: AsyncKernel) -> None: + response = await async_client.apps.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + app = await response.parse() + assert_matches_type(AppListResponse, app, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: + async with async_client.apps.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + app = await response.parse() + assert_matches_type(AppListResponse, app, path=["response"]) + + assert cast(Any, response.is_closed) is True From 766a5e9a56e95bf176ae0be5fa4b0f009bf9827e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 21:38:57 +0000 Subject: [PATCH 039/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index ee49ac2d..fd0ccba9 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.11" + ".": "0.1.0-alpha.12" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 7f1d0e7a..3871d47e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.1.0-alpha.11" +version = "0.1.0-alpha.12" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index ec24ea7c..02886717 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.1.0-alpha.11" # x-release-please-version +__version__ = "0.1.0-alpha.12" # x-release-please-version From c963510f441e3977183e44622d8b3e433b7b70ed Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 20 May 2025 15:57:59 +0000 Subject: [PATCH 040/448] feat(api): update via SDK Studio --- .stats.yml | 8 +- api.md | 10 --- src/kernel/resources/apps/apps.py | 125 -------------------------- src/kernel/types/__init__.py | 2 - src/kernel/types/app_list_params.py | 15 ---- src/kernel/types/app_list_response.py | 28 ------ tests/api_resources/test_apps.py | 96 -------------------- 7 files changed, 4 insertions(+), 280 deletions(-) delete mode 100644 src/kernel/types/app_list_params.py delete mode 100644 src/kernel/types/app_list_response.py delete mode 100644 tests/api_resources/test_apps.py diff --git a/.stats.yml b/.stats.yml index 01c41add..f0d8544e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-c9d64df733f286f09d2203f4e3d820ce57e8d4c629c5e2db4e2bfac91fbc1598.yml -openapi_spec_hash: fa407611fc566d55f403864fbfaa6c23 -config_hash: 7f67c5b95af1e4b39525515240b72275 +configured_endpoints: 6 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-19b0d17ba368f32827ee322d15a7f4ff7e1f3bbf66606fad227b3465f8ffc5ab.yml +openapi_spec_hash: 4a3cb766898e8a134ef99fe6c4c87736 +config_hash: 4dfa4d870ce0e23e31ce33ab6a53dd21 diff --git a/api.md b/api.md index 63d7b00d..32a9bb03 100644 --- a/api.md +++ b/api.md @@ -1,15 +1,5 @@ # Apps -Types: - -```python -from kernel.types import AppListResponse -``` - -Methods: - -- client.apps.list(\*\*params) -> AppListResponse - ## Deployments Types: diff --git a/src/kernel/resources/apps/apps.py b/src/kernel/resources/apps/apps.py index 9a5f6670..848b765b 100644 --- a/src/kernel/resources/apps/apps.py +++ b/src/kernel/resources/apps/apps.py @@ -2,19 +2,8 @@ from __future__ import annotations -import httpx - -from ...types import app_list_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 .deployments import ( DeploymentsResource, AsyncDeploymentsResource, @@ -31,8 +20,6 @@ InvocationsResourceWithStreamingResponse, AsyncInvocationsResourceWithStreamingResponse, ) -from ..._base_client import make_request_options -from ...types.app_list_response import AppListResponse __all__ = ["AppsResource", "AsyncAppsResource"] @@ -65,54 +52,6 @@ def with_streaming_response(self) -> AppsResourceWithStreamingResponse: """ return AppsResourceWithStreamingResponse(self) - def list( - self, - *, - app_name: str | NotGiven = NOT_GIVEN, - version: 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, - ) -> AppListResponse: - """List application versions for the authenticated user. - - Optionally filter by app - name and/or version label. - - Args: - app_name: Filter results by application name. - - version: Filter results by version label. - - 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( - "/apps", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=maybe_transform( - { - "app_name": app_name, - "version": version, - }, - app_list_params.AppListParams, - ), - ), - cast_to=AppListResponse, - ) - class AsyncAppsResource(AsyncAPIResource): @cached_property @@ -142,63 +81,11 @@ def with_streaming_response(self) -> AsyncAppsResourceWithStreamingResponse: """ return AsyncAppsResourceWithStreamingResponse(self) - async def list( - self, - *, - app_name: str | NotGiven = NOT_GIVEN, - version: 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, - ) -> AppListResponse: - """List application versions for the authenticated user. - - Optionally filter by app - name and/or version label. - - Args: - app_name: Filter results by application name. - - version: Filter results by version label. - - 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( - "/apps", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=await async_maybe_transform( - { - "app_name": app_name, - "version": version, - }, - app_list_params.AppListParams, - ), - ), - cast_to=AppListResponse, - ) - class AppsResourceWithRawResponse: def __init__(self, apps: AppsResource) -> None: self._apps = apps - self.list = to_raw_response_wrapper( - apps.list, - ) - @cached_property def deployments(self) -> DeploymentsResourceWithRawResponse: return DeploymentsResourceWithRawResponse(self._apps.deployments) @@ -212,10 +99,6 @@ class AsyncAppsResourceWithRawResponse: def __init__(self, apps: AsyncAppsResource) -> None: self._apps = apps - self.list = async_to_raw_response_wrapper( - apps.list, - ) - @cached_property def deployments(self) -> AsyncDeploymentsResourceWithRawResponse: return AsyncDeploymentsResourceWithRawResponse(self._apps.deployments) @@ -229,10 +112,6 @@ class AppsResourceWithStreamingResponse: def __init__(self, apps: AppsResource) -> None: self._apps = apps - self.list = to_streamed_response_wrapper( - apps.list, - ) - @cached_property def deployments(self) -> DeploymentsResourceWithStreamingResponse: return DeploymentsResourceWithStreamingResponse(self._apps.deployments) @@ -246,10 +125,6 @@ class AsyncAppsResourceWithStreamingResponse: def __init__(self, apps: AsyncAppsResource) -> None: self._apps = apps - self.list = async_to_streamed_response_wrapper( - apps.list, - ) - @cached_property def deployments(self) -> AsyncDeploymentsResourceWithStreamingResponse: return AsyncDeploymentsResourceWithStreamingResponse(self._apps.deployments) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index e7c3cecd..282c8899 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -2,8 +2,6 @@ from __future__ import annotations -from .app_list_params import AppListParams as AppListParams -from .app_list_response import AppListResponse as AppListResponse from .browser_create_params import BrowserCreateParams as BrowserCreateParams from .browser_create_response import BrowserCreateResponse as BrowserCreateResponse from .browser_retrieve_response import BrowserRetrieveResponse as BrowserRetrieveResponse diff --git a/src/kernel/types/app_list_params.py b/src/kernel/types/app_list_params.py deleted file mode 100644 index d4506a3e..00000000 --- a/src/kernel/types/app_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 TypedDict - -__all__ = ["AppListParams"] - - -class AppListParams(TypedDict, total=False): - app_name: str - """Filter results by application name.""" - - version: str - """Filter results by version label.""" diff --git a/src/kernel/types/app_list_response.py b/src/kernel/types/app_list_response.py deleted file mode 100644 index 8a6f6218..00000000 --- a/src/kernel/types/app_list_response.py +++ /dev/null @@ -1,28 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Dict, List, Optional -from typing_extensions import TypeAlias - -from .._models import BaseModel - -__all__ = ["AppListResponse", "AppListResponseItem"] - - -class AppListResponseItem(BaseModel): - id: str - """Unique identifier for the app version""" - - app_name: str - """Name of the application""" - - region: str - """Deployment region code""" - - version: str - """Version label for the application""" - - env_vars: Optional[Dict[str, str]] = None - """Environment variables configured for this app version""" - - -AppListResponse: TypeAlias = List[AppListResponseItem] diff --git a/tests/api_resources/test_apps.py b/tests/api_resources/test_apps.py deleted file mode 100644 index b902576a..00000000 --- a/tests/api_resources/test_apps.py +++ /dev/null @@ -1,96 +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 kernel import Kernel, AsyncKernel -from tests.utils import assert_matches_type -from kernel.types import AppListResponse - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestApps: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip() - @parametrize - def test_method_list(self, client: Kernel) -> None: - app = client.apps.list() - assert_matches_type(AppListResponse, app, path=["response"]) - - @pytest.mark.skip() - @parametrize - def test_method_list_with_all_params(self, client: Kernel) -> None: - app = client.apps.list( - app_name="app_name", - version="version", - ) - assert_matches_type(AppListResponse, app, path=["response"]) - - @pytest.mark.skip() - @parametrize - def test_raw_response_list(self, client: Kernel) -> None: - response = client.apps.with_raw_response.list() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - app = response.parse() - assert_matches_type(AppListResponse, app, path=["response"]) - - @pytest.mark.skip() - @parametrize - def test_streaming_response_list(self, client: Kernel) -> None: - with client.apps.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - app = response.parse() - assert_matches_type(AppListResponse, app, path=["response"]) - - assert cast(Any, response.is_closed) is True - - -class TestAsyncApps: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip() - @parametrize - async def test_method_list(self, async_client: AsyncKernel) -> None: - app = await async_client.apps.list() - assert_matches_type(AppListResponse, app, path=["response"]) - - @pytest.mark.skip() - @parametrize - async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: - app = await async_client.apps.list( - app_name="app_name", - version="version", - ) - assert_matches_type(AppListResponse, app, path=["response"]) - - @pytest.mark.skip() - @parametrize - async def test_raw_response_list(self, async_client: AsyncKernel) -> None: - response = await async_client.apps.with_raw_response.list() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - app = await response.parse() - assert_matches_type(AppListResponse, app, path=["response"]) - - @pytest.mark.skip() - @parametrize - async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: - async with async_client.apps.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - app = await response.parse() - assert_matches_type(AppListResponse, app, path=["response"]) - - assert cast(Any, response.is_closed) is True From f0f78fd13e580b4f536c4559bba5bb4e78c044ed Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 20 May 2025 16:02:09 +0000 Subject: [PATCH 041/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index fd0ccba9..000572ec 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.12" + ".": "0.1.0-alpha.13" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 3871d47e..4e20f732 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.1.0-alpha.12" +version = "0.1.0-alpha.13" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 02886717..d48ee69d 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.1.0-alpha.12" # x-release-please-version +__version__ = "0.1.0-alpha.13" # x-release-please-version From 4ce5003c94e7fc4c4f7035a8288f34c7cd6e7dda Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 20 May 2025 16:58:57 +0000 Subject: [PATCH 042/448] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index f0d8544e..f8cfc706 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 6 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-19b0d17ba368f32827ee322d15a7f4ff7e1f3bbf66606fad227b3465f8ffc5ab.yml openapi_spec_hash: 4a3cb766898e8a134ef99fe6c4c87736 -config_hash: 4dfa4d870ce0e23e31ce33ab6a53dd21 +config_hash: 5b3919927cba9bf9dcc80458c199318d From 1be7c637741286e42c3828524c8b687a8f193e1b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 20 May 2025 17:00:41 +0000 Subject: [PATCH 043/448] feat(api): update via SDK Studio --- .stats.yml | 8 +- api.md | 10 +++ src/kernel/resources/apps/apps.py | 125 ++++++++++++++++++++++++++ src/kernel/types/__init__.py | 2 + src/kernel/types/app_list_params.py | 15 ++++ src/kernel/types/app_list_response.py | 28 ++++++ tests/api_resources/test_apps.py | 96 ++++++++++++++++++++ 7 files changed, 280 insertions(+), 4 deletions(-) create mode 100644 src/kernel/types/app_list_params.py create mode 100644 src/kernel/types/app_list_response.py create mode 100644 tests/api_resources/test_apps.py diff --git a/.stats.yml b/.stats.yml index f8cfc706..2b23cedd 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 6 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-19b0d17ba368f32827ee322d15a7f4ff7e1f3bbf66606fad227b3465f8ffc5ab.yml -openapi_spec_hash: 4a3cb766898e8a134ef99fe6c4c87736 -config_hash: 5b3919927cba9bf9dcc80458c199318d +configured_endpoints: 7 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-c9d64df733f286f09d2203f4e3d820ce57e8d4c629c5e2db4e2bfac91fbc1598.yml +openapi_spec_hash: fa407611fc566d55f403864fbfaa6c23 +config_hash: 4dfa4d870ce0e23e31ce33ab6a53dd21 diff --git a/api.md b/api.md index 32a9bb03..63d7b00d 100644 --- a/api.md +++ b/api.md @@ -1,5 +1,15 @@ # Apps +Types: + +```python +from kernel.types import AppListResponse +``` + +Methods: + +- client.apps.list(\*\*params) -> AppListResponse + ## Deployments Types: diff --git a/src/kernel/resources/apps/apps.py b/src/kernel/resources/apps/apps.py index 848b765b..9a5f6670 100644 --- a/src/kernel/resources/apps/apps.py +++ b/src/kernel/resources/apps/apps.py @@ -2,8 +2,19 @@ from __future__ import annotations +import httpx + +from ...types import app_list_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 .deployments import ( DeploymentsResource, AsyncDeploymentsResource, @@ -20,6 +31,8 @@ InvocationsResourceWithStreamingResponse, AsyncInvocationsResourceWithStreamingResponse, ) +from ..._base_client import make_request_options +from ...types.app_list_response import AppListResponse __all__ = ["AppsResource", "AsyncAppsResource"] @@ -52,6 +65,54 @@ def with_streaming_response(self) -> AppsResourceWithStreamingResponse: """ return AppsResourceWithStreamingResponse(self) + def list( + self, + *, + app_name: str | NotGiven = NOT_GIVEN, + version: 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, + ) -> AppListResponse: + """List application versions for the authenticated user. + + Optionally filter by app + name and/or version label. + + Args: + app_name: Filter results by application name. + + version: Filter results by version label. + + 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( + "/apps", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "app_name": app_name, + "version": version, + }, + app_list_params.AppListParams, + ), + ), + cast_to=AppListResponse, + ) + class AsyncAppsResource(AsyncAPIResource): @cached_property @@ -81,11 +142,63 @@ def with_streaming_response(self) -> AsyncAppsResourceWithStreamingResponse: """ return AsyncAppsResourceWithStreamingResponse(self) + async def list( + self, + *, + app_name: str | NotGiven = NOT_GIVEN, + version: 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, + ) -> AppListResponse: + """List application versions for the authenticated user. + + Optionally filter by app + name and/or version label. + + Args: + app_name: Filter results by application name. + + version: Filter results by version label. + + 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( + "/apps", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "app_name": app_name, + "version": version, + }, + app_list_params.AppListParams, + ), + ), + cast_to=AppListResponse, + ) + class AppsResourceWithRawResponse: def __init__(self, apps: AppsResource) -> None: self._apps = apps + self.list = to_raw_response_wrapper( + apps.list, + ) + @cached_property def deployments(self) -> DeploymentsResourceWithRawResponse: return DeploymentsResourceWithRawResponse(self._apps.deployments) @@ -99,6 +212,10 @@ class AsyncAppsResourceWithRawResponse: def __init__(self, apps: AsyncAppsResource) -> None: self._apps = apps + self.list = async_to_raw_response_wrapper( + apps.list, + ) + @cached_property def deployments(self) -> AsyncDeploymentsResourceWithRawResponse: return AsyncDeploymentsResourceWithRawResponse(self._apps.deployments) @@ -112,6 +229,10 @@ class AppsResourceWithStreamingResponse: def __init__(self, apps: AppsResource) -> None: self._apps = apps + self.list = to_streamed_response_wrapper( + apps.list, + ) + @cached_property def deployments(self) -> DeploymentsResourceWithStreamingResponse: return DeploymentsResourceWithStreamingResponse(self._apps.deployments) @@ -125,6 +246,10 @@ class AsyncAppsResourceWithStreamingResponse: def __init__(self, apps: AsyncAppsResource) -> None: self._apps = apps + self.list = async_to_streamed_response_wrapper( + apps.list, + ) + @cached_property def deployments(self) -> AsyncDeploymentsResourceWithStreamingResponse: return AsyncDeploymentsResourceWithStreamingResponse(self._apps.deployments) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 282c8899..e7c3cecd 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -2,6 +2,8 @@ from __future__ import annotations +from .app_list_params import AppListParams as AppListParams +from .app_list_response import AppListResponse as AppListResponse from .browser_create_params import BrowserCreateParams as BrowserCreateParams from .browser_create_response import BrowserCreateResponse as BrowserCreateResponse from .browser_retrieve_response import BrowserRetrieveResponse as BrowserRetrieveResponse diff --git a/src/kernel/types/app_list_params.py b/src/kernel/types/app_list_params.py new file mode 100644 index 00000000..d4506a3e --- /dev/null +++ b/src/kernel/types/app_list_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["AppListParams"] + + +class AppListParams(TypedDict, total=False): + app_name: str + """Filter results by application name.""" + + version: str + """Filter results by version label.""" diff --git a/src/kernel/types/app_list_response.py b/src/kernel/types/app_list_response.py new file mode 100644 index 00000000..8a6f6218 --- /dev/null +++ b/src/kernel/types/app_list_response.py @@ -0,0 +1,28 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, List, Optional +from typing_extensions import TypeAlias + +from .._models import BaseModel + +__all__ = ["AppListResponse", "AppListResponseItem"] + + +class AppListResponseItem(BaseModel): + id: str + """Unique identifier for the app version""" + + app_name: str + """Name of the application""" + + region: str + """Deployment region code""" + + version: str + """Version label for the application""" + + env_vars: Optional[Dict[str, str]] = None + """Environment variables configured for this app version""" + + +AppListResponse: TypeAlias = List[AppListResponseItem] diff --git a/tests/api_resources/test_apps.py b/tests/api_resources/test_apps.py new file mode 100644 index 00000000..b902576a --- /dev/null +++ b/tests/api_resources/test_apps.py @@ -0,0 +1,96 @@ +# 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 kernel import Kernel, AsyncKernel +from tests.utils import assert_matches_type +from kernel.types import AppListResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestApps: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip() + @parametrize + def test_method_list(self, client: Kernel) -> None: + app = client.apps.list() + assert_matches_type(AppListResponse, app, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_method_list_with_all_params(self, client: Kernel) -> None: + app = client.apps.list( + app_name="app_name", + version="version", + ) + assert_matches_type(AppListResponse, app, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_raw_response_list(self, client: Kernel) -> None: + response = client.apps.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + app = response.parse() + assert_matches_type(AppListResponse, app, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_streaming_response_list(self, client: Kernel) -> None: + with client.apps.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + app = response.parse() + assert_matches_type(AppListResponse, app, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncApps: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip() + @parametrize + async def test_method_list(self, async_client: AsyncKernel) -> None: + app = await async_client.apps.list() + assert_matches_type(AppListResponse, app, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: + app = await async_client.apps.list( + app_name="app_name", + version="version", + ) + assert_matches_type(AppListResponse, app, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_raw_response_list(self, async_client: AsyncKernel) -> None: + response = await async_client.apps.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + app = await response.parse() + assert_matches_type(AppListResponse, app, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: + async with async_client.apps.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + app = await response.parse() + assert_matches_type(AppListResponse, app, path=["response"]) + + assert cast(Any, response.is_closed) is True From 4e0c526c4e108671c862861861df3731d252cfd0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 20 May 2025 17:02:40 +0000 Subject: [PATCH 044/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 000572ec..b0699969 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.13" + ".": "0.1.0-alpha.14" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 4e20f732..ad8a73b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.1.0-alpha.13" +version = "0.1.0-alpha.14" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index d48ee69d..3ee690e4 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.1.0-alpha.13" # x-release-please-version +__version__ = "0.1.0-alpha.14" # x-release-please-version From b6e6dd7ab8a60ccdbb06e2a7f888ea1f784363fc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 20 May 2025 18:19:00 +0000 Subject: [PATCH 045/448] feat(api): update via SDK Studio --- .stats.yml | 4 +- api.md | 2 +- src/kernel/resources/apps/deployments.py | 13 ++-- src/kernel/types/apps/__init__.py | 1 - .../types/apps/deployment_follow_response.py | 63 ------------------- 5 files changed, 9 insertions(+), 74 deletions(-) delete mode 100644 src/kernel/types/apps/deployment_follow_response.py diff --git a/.stats.yml b/.stats.yml index 2b23cedd..e44b3a1a 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-c9d64df733f286f09d2203f4e3d820ce57e8d4c629c5e2db4e2bfac91fbc1598.yml -openapi_spec_hash: fa407611fc566d55f403864fbfaa6c23 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-aa34ccb9b2ee8e81ef56881ff7474ea2d69a059d5c1dbb7d0ec94e28a0b68559.yml +openapi_spec_hash: c573fcd85b195ebe809a1039634652d6 config_hash: 4dfa4d870ce0e23e31ce33ab6a53dd21 diff --git a/api.md b/api.md index 63d7b00d..33435d97 100644 --- a/api.md +++ b/api.md @@ -21,7 +21,7 @@ from kernel.types.apps import DeploymentCreateResponse, DeploymentFollowResponse Methods: - client.apps.deployments.create(\*\*params) -> DeploymentCreateResponse -- client.apps.deployments.follow(id) -> DeploymentFollowResponse +- client.apps.deployments.follow(id) -> object ## Invocations diff --git a/src/kernel/resources/apps/deployments.py b/src/kernel/resources/apps/deployments.py index 9a1cecaf..8405280a 100644 --- a/src/kernel/resources/apps/deployments.py +++ b/src/kernel/resources/apps/deployments.py @@ -21,7 +21,6 @@ from ...types.apps import deployment_create_params from ..._base_client import make_request_options from ...types.apps.deployment_create_response import DeploymentCreateResponse -from ...types.apps.deployment_follow_response import DeploymentFollowResponse __all__ = ["DeploymentsResource", "AsyncDeploymentsResource"] @@ -122,7 +121,7 @@ def follow( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Stream[DeploymentFollowResponse]: + ) -> Stream[object]: """ Establishes a Server-Sent Events (SSE) stream that delivers real-time logs and status updates for a deployed application. The stream terminates automatically @@ -145,9 +144,9 @@ def follow( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=DeploymentFollowResponse, + cast_to=object, stream=True, - stream_cls=Stream[DeploymentFollowResponse], + stream_cls=Stream[object], ) @@ -247,7 +246,7 @@ async def follow( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AsyncStream[DeploymentFollowResponse]: + ) -> AsyncStream[object]: """ Establishes a Server-Sent Events (SSE) stream that delivers real-time logs and status updates for a deployed application. The stream terminates automatically @@ -270,9 +269,9 @@ async def follow( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=DeploymentFollowResponse, + cast_to=object, stream=True, - stream_cls=AsyncStream[DeploymentFollowResponse], + stream_cls=AsyncStream[object], ) diff --git a/src/kernel/types/apps/__init__.py b/src/kernel/types/apps/__init__.py index 425fffd4..f4d451cf 100644 --- a/src/kernel/types/apps/__init__.py +++ b/src/kernel/types/apps/__init__.py @@ -5,6 +5,5 @@ from .deployment_create_params import DeploymentCreateParams as DeploymentCreateParams from .invocation_create_params import InvocationCreateParams as InvocationCreateParams from .deployment_create_response import DeploymentCreateResponse as DeploymentCreateResponse -from .deployment_follow_response import DeploymentFollowResponse as DeploymentFollowResponse from .invocation_create_response import InvocationCreateResponse as InvocationCreateResponse from .invocation_retrieve_response import InvocationRetrieveResponse as InvocationRetrieveResponse diff --git a/src/kernel/types/apps/deployment_follow_response.py b/src/kernel/types/apps/deployment_follow_response.py deleted file mode 100644 index 4374485b..00000000 --- a/src/kernel/types/apps/deployment_follow_response.py +++ /dev/null @@ -1,63 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Union, Optional -from datetime import datetime -from typing_extensions import Literal, Annotated, TypeAlias - -from ..._utils import PropertyInfo -from ..._models import BaseModel - -__all__ = [ - "DeploymentFollowResponse", - "DeploymentFollowResponseItem", - "DeploymentFollowResponseItemStateEvent", - "DeploymentFollowResponseItemStateUpdateEvent", - "DeploymentFollowResponseItemLogEvent", -] - - -class DeploymentFollowResponseItemStateEvent(BaseModel): - event: Literal["state"] - """Event type identifier (always "state").""" - - state: str - """ - Current application state (e.g., "deploying", "running", "succeeded", "failed"). - """ - - timestamp: Optional[datetime] = None - """Time the state was reported.""" - - -class DeploymentFollowResponseItemStateUpdateEvent(BaseModel): - event: Literal["state_update"] - """Event type identifier (always "state_update").""" - - state: str - """New application state (e.g., "running", "succeeded", "failed").""" - - timestamp: Optional[datetime] = None - """Time the state change occurred.""" - - -class DeploymentFollowResponseItemLogEvent(BaseModel): - event: Literal["log"] - """Event type identifier (always "log").""" - - message: str - """Log message text.""" - - timestamp: Optional[datetime] = None - """Time the log entry was produced.""" - - -DeploymentFollowResponseItem: TypeAlias = Annotated[ - Union[ - DeploymentFollowResponseItemStateEvent, - DeploymentFollowResponseItemStateUpdateEvent, - DeploymentFollowResponseItemLogEvent, - ], - PropertyInfo(discriminator="event"), -] - -DeploymentFollowResponse: TypeAlias = List[DeploymentFollowResponseItem] From 683ec68e4d2a4a1741dd9d90da846ed01ec41bf9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 20 May 2025 18:20:28 +0000 Subject: [PATCH 046/448] feat(api): update via SDK Studio --- .stats.yml | 4 +- api.md | 2 +- src/kernel/resources/apps/deployments.py | 19 ++++--- src/kernel/types/apps/__init__.py | 1 + .../types/apps/deployment_follow_response.py | 50 +++++++++++++++++++ 5 files changed, 66 insertions(+), 10 deletions(-) create mode 100644 src/kernel/types/apps/deployment_follow_response.py diff --git a/.stats.yml b/.stats.yml index e44b3a1a..24a77a34 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-aa34ccb9b2ee8e81ef56881ff7474ea2d69a059d5c1dbb7d0ec94e28a0b68559.yml -openapi_spec_hash: c573fcd85b195ebe809a1039634652d6 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-39aa058a60035c34a636e7f580b4b9c76b05400ae401ef04a761572b20a5425b.yml +openapi_spec_hash: bb79a204f9edb6b6ccfe783a0a82a423 config_hash: 4dfa4d870ce0e23e31ce33ab6a53dd21 diff --git a/api.md b/api.md index 33435d97..63d7b00d 100644 --- a/api.md +++ b/api.md @@ -21,7 +21,7 @@ from kernel.types.apps import DeploymentCreateResponse, DeploymentFollowResponse Methods: - client.apps.deployments.create(\*\*params) -> DeploymentCreateResponse -- client.apps.deployments.follow(id) -> object +- client.apps.deployments.follow(id) -> DeploymentFollowResponse ## Invocations diff --git a/src/kernel/resources/apps/deployments.py b/src/kernel/resources/apps/deployments.py index 8405280a..a3e364aa 100644 --- a/src/kernel/resources/apps/deployments.py +++ b/src/kernel/resources/apps/deployments.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Dict, Mapping, cast +from typing import Any, Dict, Mapping, cast from typing_extensions import Literal import httpx @@ -21,6 +21,7 @@ from ...types.apps import deployment_create_params from ..._base_client import make_request_options from ...types.apps.deployment_create_response import DeploymentCreateResponse +from ...types.apps.deployment_follow_response import DeploymentFollowResponse __all__ = ["DeploymentsResource", "AsyncDeploymentsResource"] @@ -121,7 +122,7 @@ def follow( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Stream[object]: + ) -> Stream[DeploymentFollowResponse]: """ Establishes a Server-Sent Events (SSE) stream that delivers real-time logs and status updates for a deployed application. The stream terminates automatically @@ -144,9 +145,11 @@ def follow( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=object, + cast_to=cast( + Any, DeploymentFollowResponse + ), # Union types cannot be passed in as arguments in the type system stream=True, - stream_cls=Stream[object], + stream_cls=Stream[DeploymentFollowResponse], ) @@ -246,7 +249,7 @@ async def follow( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AsyncStream[object]: + ) -> AsyncStream[DeploymentFollowResponse]: """ Establishes a Server-Sent Events (SSE) stream that delivers real-time logs and status updates for a deployed application. The stream terminates automatically @@ -269,9 +272,11 @@ async def follow( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=object, + cast_to=cast( + Any, DeploymentFollowResponse + ), # Union types cannot be passed in as arguments in the type system stream=True, - stream_cls=AsyncStream[object], + stream_cls=AsyncStream[DeploymentFollowResponse], ) diff --git a/src/kernel/types/apps/__init__.py b/src/kernel/types/apps/__init__.py index f4d451cf..425fffd4 100644 --- a/src/kernel/types/apps/__init__.py +++ b/src/kernel/types/apps/__init__.py @@ -5,5 +5,6 @@ from .deployment_create_params import DeploymentCreateParams as DeploymentCreateParams from .invocation_create_params import InvocationCreateParams as InvocationCreateParams from .deployment_create_response import DeploymentCreateResponse as DeploymentCreateResponse +from .deployment_follow_response import DeploymentFollowResponse as DeploymentFollowResponse from .invocation_create_response import InvocationCreateResponse as InvocationCreateResponse from .invocation_retrieve_response import InvocationRetrieveResponse as InvocationRetrieveResponse diff --git a/src/kernel/types/apps/deployment_follow_response.py b/src/kernel/types/apps/deployment_follow_response.py new file mode 100644 index 00000000..eb1ded77 --- /dev/null +++ b/src/kernel/types/apps/deployment_follow_response.py @@ -0,0 +1,50 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Union, Optional +from datetime import datetime +from typing_extensions import Literal, Annotated, TypeAlias + +from ..._utils import PropertyInfo +from ..._models import BaseModel + +__all__ = ["DeploymentFollowResponse", "StateEvent", "StateUpdateEvent", "LogEvent"] + + +class StateEvent(BaseModel): + event: Literal["state"] + """Event type identifier (always "state").""" + + state: str + """ + Current application state (e.g., "deploying", "running", "succeeded", "failed"). + """ + + timestamp: Optional[datetime] = None + """Time the state was reported.""" + + +class StateUpdateEvent(BaseModel): + event: Literal["state_update"] + """Event type identifier (always "state_update").""" + + state: str + """New application state (e.g., "running", "succeeded", "failed").""" + + timestamp: Optional[datetime] = None + """Time the state change occurred.""" + + +class LogEvent(BaseModel): + event: Literal["log"] + """Event type identifier (always "log").""" + + message: str + """Log message text.""" + + timestamp: Optional[datetime] = None + """Time the log entry was produced.""" + + +DeploymentFollowResponse: TypeAlias = Annotated[ + Union[StateEvent, StateUpdateEvent, LogEvent], PropertyInfo(discriminator="event") +] From 52acdb198adb61ae305f451baaffbcde30736317 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 20 May 2025 18:22:25 +0000 Subject: [PATCH 047/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index b0699969..08e82c45 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.14" + ".": "0.1.0-alpha.15" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index ad8a73b8..0440171f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.1.0-alpha.14" +version = "0.1.0-alpha.15" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 3ee690e4..1f7f2721 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.1.0-alpha.14" # x-release-please-version +__version__ = "0.1.0-alpha.15" # x-release-please-version From a0a0cd8297d78eca374c38c1bc830e1d0924b0d1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 21 May 2025 17:59:51 +0000 Subject: [PATCH 048/448] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 24a77a34..b26bed31 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-39aa058a60035c34a636e7f580b4b9c76b05400ae401ef04a761572b20a5425b.yml openapi_spec_hash: bb79a204f9edb6b6ccfe783a0a82a423 -config_hash: 4dfa4d870ce0e23e31ce33ab6a53dd21 +config_hash: 3eb1ed1dd0067258984b31d53a0dab48 From faddcb8f6bd55c636fa9d98c3c44c34b4f910a99 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 21 May 2025 18:02:17 +0000 Subject: [PATCH 049/448] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index b26bed31..b30a9668 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-39aa058a60035c34a636e7f580b4b9c76b05400ae401ef04a761572b20a5425b.yml -openapi_spec_hash: bb79a204f9edb6b6ccfe783a0a82a423 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-b594ec244fa0dd448274eb91c93f8c43f19b5056415e457e1e90fa9df749b52a.yml +openapi_spec_hash: 6bd4844a6e85289bea205723dbafff58 config_hash: 3eb1ed1dd0067258984b31d53a0dab48 From 5988c82cdec4a821a9e385e15de12e2c14f82c73 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 21 May 2025 18:04:12 +0000 Subject: [PATCH 050/448] feat(api): update via SDK Studio --- .stats.yml | 6 +++--- README.md | 10 ++++++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.stats.yml b/.stats.yml index b30a9668..bf7a9321 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-b594ec244fa0dd448274eb91c93f8c43f19b5056415e457e1e90fa9df749b52a.yml -openapi_spec_hash: 6bd4844a6e85289bea205723dbafff58 -config_hash: 3eb1ed1dd0067258984b31d53a0dab48 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-39aa058a60035c34a636e7f580b4b9c76b05400ae401ef04a761572b20a5425b.yml +openapi_spec_hash: bb79a204f9edb6b6ccfe783a0a82a423 +config_hash: 5c90b7df80e8f222bb945b14b8d1fec0 diff --git a/README.md b/README.md index b9a0ae80..6084c029 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,10 @@ client = Kernel( deployment = client.apps.deployments.create( entrypoint_rel_path="main.ts", file=b"REPLACE_ME", - env_vars={"OPENAI_API_KEY": "x"}, + env_vars={ + "OPENAI_API_KEY": "x", + "LOG_LEVEL": "debug", + }, version="1.0.0", ) print(deployment.apps) @@ -67,7 +70,10 @@ async def main() -> None: deployment = await client.apps.deployments.create( entrypoint_rel_path="main.ts", file=b"REPLACE_ME", - env_vars={"OPENAI_API_KEY": "x"}, + env_vars={ + "OPENAI_API_KEY": "x", + "LOG_LEVEL": "debug", + }, version="1.0.0", ) print(deployment.apps) From 244f684c7a2bbd3a195f4fdaea80777fb3f0c62c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 21 May 2025 18:07:16 +0000 Subject: [PATCH 051/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- README.md | 2 +- pyproject.toml | 2 +- scripts/utils/upload-artifact.sh | 2 +- src/kernel/_version.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 08e82c45..3d2ac0bd 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.15" + ".": "0.1.0" } \ No newline at end of file diff --git a/README.md b/README.md index 6084c029..df3236b5 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The REST API documentation can be found on [docs.onkernel.com](https://docs.onke ```sh # install from PyPI -pip install --pre kernel +pip install kernel ``` ## Usage diff --git a/pyproject.toml b/pyproject.toml index 0440171f..ac353b9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.1.0-alpha.15" +version = "0.1.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh index c55ebbca..7b344b4f 100755 --- a/scripts/utils/upload-artifact.sh +++ b/scripts/utils/upload-artifact.sh @@ -18,7 +18,7 @@ UPLOAD_RESPONSE=$(tar -cz . | curl -v -X PUT \ if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then echo -e "\033[32mUploaded build to Stainless storage.\033[0m" - echo -e "\033[32mInstallation: pip install --pre 'https://pkg.stainless.com/s/kernel-python/$SHA'\033[0m" + echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/kernel-python/$SHA'\033[0m" else echo -e "\033[31mFailed to upload artifact.\033[0m" exit 1 diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 1f7f2721..5d07ad99 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.1.0-alpha.15" # x-release-please-version +__version__ = "0.1.0" # x-release-please-version From 2b865d829d656ab4f9df2c4c91dfde1c8166bb56 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 21 May 2025 18:11:09 +0000 Subject: [PATCH 052/448] feat(api): update via SDK Studio --- .stats.yml | 2 +- README.md | 10 ++-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/.stats.yml b/.stats.yml index bf7a9321..b26bed31 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-39aa058a60035c34a636e7f580b4b9c76b05400ae401ef04a761572b20a5425b.yml openapi_spec_hash: bb79a204f9edb6b6ccfe783a0a82a423 -config_hash: 5c90b7df80e8f222bb945b14b8d1fec0 +config_hash: 3eb1ed1dd0067258984b31d53a0dab48 diff --git a/README.md b/README.md index df3236b5..cd2df758 100644 --- a/README.md +++ b/README.md @@ -36,10 +36,7 @@ client = Kernel( deployment = client.apps.deployments.create( entrypoint_rel_path="main.ts", file=b"REPLACE_ME", - env_vars={ - "OPENAI_API_KEY": "x", - "LOG_LEVEL": "debug", - }, + env_vars={"OPENAI_API_KEY": "x"}, version="1.0.0", ) print(deployment.apps) @@ -70,10 +67,7 @@ async def main() -> None: deployment = await client.apps.deployments.create( entrypoint_rel_path="main.ts", file=b"REPLACE_ME", - env_vars={ - "OPENAI_API_KEY": "x", - "LOG_LEVEL": "debug", - }, + env_vars={"OPENAI_API_KEY": "x"}, version="1.0.0", ) print(deployment.apps) From fe96ffa332742fb34f875155206d228b7a125fc1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 21 May 2025 18:12:21 +0000 Subject: [PATCH 053/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 3d2ac0bd..10f30916 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0" + ".": "0.2.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index ac353b9f..034e1164 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.1.0" +version = "0.2.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 5d07ad99..4f726fa3 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.1.0" # x-release-please-version +__version__ = "0.2.0" # x-release-please-version From f6bd38be917d38ae51d94d1ae344b5e89e5eb2bf Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 02:31:59 +0000 Subject: [PATCH 054/448] chore(docs): grammar improvements --- SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SECURITY.md b/SECURITY.md index bd2ba47d..0c6c32d1 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -16,7 +16,7 @@ before making any information public. ## Reporting Non-SDK Related Security Issues If you encounter security issues that are not directly related to SDKs but pertain to the services -or products provided by Kernel please follow the respective company's security reporting guidelines. +or products provided by Kernel, please follow the respective company's security reporting guidelines. --- From 058af0457a228477106a82b8f03903e492de1c0b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 15:25:41 +0000 Subject: [PATCH 055/448] feat(api): update via SDK Studio --- .stats.yml | 4 ++-- README.md | 16 +++++++++++++++ src/kernel/resources/browsers.py | 20 +++++++++++++++++-- src/kernel/types/browser_create_params.py | 10 +++++++++- src/kernel/types/browser_create_response.py | 12 ++++++++++- src/kernel/types/browser_retrieve_response.py | 12 ++++++++++- tests/api_resources/test_browsers.py | 18 +++++++++++++++++ 7 files changed, 85 insertions(+), 7 deletions(-) diff --git a/.stats.yml b/.stats.yml index b26bed31..36c603b5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-39aa058a60035c34a636e7f580b4b9c76b05400ae401ef04a761572b20a5425b.yml -openapi_spec_hash: bb79a204f9edb6b6ccfe783a0a82a423 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2813f659cb4e9e81cd3d9c94df748fd6c54f966fd6fd4881da369394aa981ace.yml +openapi_spec_hash: facb760f50156c700b5c016087a70d64 config_hash: 3eb1ed1dd0067258984b31d53a0dab48 diff --git a/README.md b/README.md index cd2df758..28474ed5 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,22 @@ Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typ Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. +## Nested params + +Nested parameters are dictionaries, typed using `TypedDict`, for example: + +```python +from kernel import Kernel + +client = Kernel() + +browser = client.browsers.create( + invocation_id="ckqwer3o20000jb9s7abcdef", + persistence={"id": "my-shared-browser"}, +) +print(browser.persistence) +``` + ## File uploads Request parameters that correspond to file uploads can be passed as `bytes`, or a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance or a tuple of `(filename, contents, media type)`. diff --git a/src/kernel/resources/browsers.py b/src/kernel/resources/browsers.py index 2aa307a8..33b0cfd1 100644 --- a/src/kernel/resources/browsers.py +++ b/src/kernel/resources/browsers.py @@ -46,6 +46,7 @@ def create( self, *, invocation_id: str, + persistence: browser_create_params.Persistence | 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, @@ -59,6 +60,8 @@ def create( Args: invocation_id: action invocation ID + persistence: Optional persistence configuration for the browser session. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -69,7 +72,13 @@ def create( """ return self._post( "/browsers", - body=maybe_transform({"invocation_id": invocation_id}, browser_create_params.BrowserCreateParams), + body=maybe_transform( + { + "invocation_id": invocation_id, + "persistence": persistence, + }, + browser_create_params.BrowserCreateParams, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -134,6 +143,7 @@ async def create( self, *, invocation_id: str, + persistence: browser_create_params.Persistence | 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, @@ -147,6 +157,8 @@ async def create( Args: invocation_id: action invocation ID + persistence: Optional persistence configuration for the browser session. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -158,7 +170,11 @@ async def create( return await self._post( "/browsers", body=await async_maybe_transform( - {"invocation_id": invocation_id}, browser_create_params.BrowserCreateParams + { + "invocation_id": invocation_id, + "persistence": persistence, + }, + browser_create_params.BrowserCreateParams, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index 0944a61e..e1f90473 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -4,9 +4,17 @@ from typing_extensions import Required, TypedDict -__all__ = ["BrowserCreateParams"] +__all__ = ["BrowserCreateParams", "Persistence"] class BrowserCreateParams(TypedDict, total=False): invocation_id: Required[str] """action invocation ID""" + + persistence: Persistence + """Optional persistence configuration for the browser session.""" + + +class Persistence(TypedDict, total=False): + id: Required[str] + """Unique identifier for the persistent browser session.""" diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index 647dfc86..a992ef65 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -1,8 +1,15 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from typing import Optional + from .._models import BaseModel -__all__ = ["BrowserCreateResponse"] +__all__ = ["BrowserCreateResponse", "Persistence"] + + +class Persistence(BaseModel): + id: str + """Unique identifier for the persistent browser session.""" class BrowserCreateResponse(BaseModel): @@ -14,3 +21,6 @@ class BrowserCreateResponse(BaseModel): session_id: str """Unique identifier for the browser session""" + + persistence: Optional[Persistence] = None + """Optional persistence configuration for the browser session.""" diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index e84bb015..7dd69e62 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -1,8 +1,15 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from typing import Optional + from .._models import BaseModel -__all__ = ["BrowserRetrieveResponse"] +__all__ = ["BrowserRetrieveResponse", "Persistence"] + + +class Persistence(BaseModel): + id: str + """Unique identifier for the persistent browser session.""" class BrowserRetrieveResponse(BaseModel): @@ -14,3 +21,6 @@ class BrowserRetrieveResponse(BaseModel): session_id: str """Unique identifier for the browser session""" + + persistence: Optional[Persistence] = None + """Optional persistence configuration for the browser session.""" diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index 91fc83ec..d4d7a078 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -25,6 +25,15 @@ def test_method_create(self, client: Kernel) -> None: ) assert_matches_type(BrowserCreateResponse, browser, path=["response"]) + @pytest.mark.skip() + @parametrize + def test_method_create_with_all_params(self, client: Kernel) -> None: + browser = client.browsers.create( + invocation_id="ckqwer3o20000jb9s7abcdef", + persistence={"id": "my-shared-browser"}, + ) + assert_matches_type(BrowserCreateResponse, browser, path=["response"]) + @pytest.mark.skip() @parametrize def test_raw_response_create(self, client: Kernel) -> None: @@ -105,6 +114,15 @@ async def test_method_create(self, async_client: AsyncKernel) -> None: ) assert_matches_type(BrowserCreateResponse, browser, path=["response"]) + @pytest.mark.skip() + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: + browser = await async_client.browsers.create( + invocation_id="ckqwer3o20000jb9s7abcdef", + persistence={"id": "my-shared-browser"}, + ) + assert_matches_type(BrowserCreateResponse, browser, path=["response"]) + @pytest.mark.skip() @parametrize async def test_raw_response_create(self, async_client: AsyncKernel) -> None: From 18b6864bc4a6cbee7be257e6de1ee0f4697d284e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 16:42:56 +0000 Subject: [PATCH 056/448] feat(api): update via SDK Studio --- .stats.yml | 8 +- api.md | 10 +- src/kernel/resources/browsers.py | 230 +++++++++++++++++- src/kernel/types/__init__.py | 4 + src/kernel/types/browser_create_params.py | 11 +- src/kernel/types/browser_create_response.py | 10 +- src/kernel/types/browser_delete_params.py | 12 + src/kernel/types/browser_list_response.py | 26 ++ src/kernel/types/browser_persistence.py | 10 + src/kernel/types/browser_persistence_param.py | 12 + src/kernel/types/browser_retrieve_response.py | 10 +- tests/api_resources/test_browsers.py | 214 +++++++++++++++- 12 files changed, 526 insertions(+), 31 deletions(-) create mode 100644 src/kernel/types/browser_delete_params.py create mode 100644 src/kernel/types/browser_list_response.py create mode 100644 src/kernel/types/browser_persistence.py create mode 100644 src/kernel/types/browser_persistence_param.py diff --git a/.stats.yml b/.stats.yml index 36c603b5..77c3857b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2813f659cb4e9e81cd3d9c94df748fd6c54f966fd6fd4881da369394aa981ace.yml -openapi_spec_hash: facb760f50156c700b5c016087a70d64 -config_hash: 3eb1ed1dd0067258984b31d53a0dab48 +configured_endpoints: 10 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-ffefc234d11c041cadab66fa6e7c379cebbd9422d38f2b1b1019e425ae19bbd8.yml +openapi_spec_hash: aa04a371ff95b44847450d657ad0a920 +config_hash: f33cc77a9c01e879ad127194c897a988 diff --git a/api.md b/api.md index 63d7b00d..e94025bc 100644 --- a/api.md +++ b/api.md @@ -41,10 +41,18 @@ Methods: Types: ```python -from kernel.types import BrowserCreateResponse, BrowserRetrieveResponse +from kernel.types import ( + BrowserPersistence, + BrowserCreateResponse, + BrowserRetrieveResponse, + BrowserListResponse, +) ``` Methods: - client.browsers.create(\*\*params) -> BrowserCreateResponse - client.browsers.retrieve(id) -> BrowserRetrieveResponse +- client.browsers.list() -> BrowserListResponse +- client.browsers.delete(\*\*params) -> None +- client.browsers.delete_by_id(id) -> None diff --git a/src/kernel/resources/browsers.py b/src/kernel/resources/browsers.py index 33b0cfd1..6816edd6 100644 --- a/src/kernel/resources/browsers.py +++ b/src/kernel/resources/browsers.py @@ -4,8 +4,8 @@ import httpx -from ..types import browser_create_params -from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ..types import browser_create_params, browser_delete_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 @@ -16,7 +16,9 @@ async_to_streamed_response_wrapper, ) from .._base_client import make_request_options +from ..types.browser_list_response import BrowserListResponse from ..types.browser_create_response import BrowserCreateResponse +from ..types.browser_persistence_param import BrowserPersistenceParam from ..types.browser_retrieve_response import BrowserRetrieveResponse __all__ = ["BrowsersResource", "AsyncBrowsersResource"] @@ -46,7 +48,7 @@ def create( self, *, invocation_id: str, - persistence: browser_create_params.Persistence | NotGiven = NOT_GIVEN, + persistence: BrowserPersistenceParam | 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, @@ -118,6 +120,97 @@ def retrieve( cast_to=BrowserRetrieveResponse, ) + def list( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BrowserListResponse: + """List active browser sessions for the authenticated user""" + return self._get( + "/browsers", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BrowserListResponse, + ) + + def delete( + self, + *, + persistent_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 persistent browser session by persistent_id query parameter. + + Args: + persistent_id: Persistent browser identifier + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + "/browsers", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform({"persistent_id": persistent_id}, browser_delete_params.BrowserDeleteParams), + ), + cast_to=NoneType, + ) + + def delete_by_id( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """ + Delete Browser Session by ID + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + f"/browsers/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + class AsyncBrowsersResource(AsyncAPIResource): @cached_property @@ -143,7 +236,7 @@ async def create( self, *, invocation_id: str, - persistence: browser_create_params.Persistence | NotGiven = NOT_GIVEN, + persistence: BrowserPersistenceParam | 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, @@ -215,6 +308,99 @@ async def retrieve( cast_to=BrowserRetrieveResponse, ) + async def list( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BrowserListResponse: + """List active browser sessions for the authenticated user""" + return await self._get( + "/browsers", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BrowserListResponse, + ) + + async def delete( + self, + *, + persistent_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 persistent browser session by persistent_id query parameter. + + Args: + persistent_id: Persistent browser identifier + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + "/browsers", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + {"persistent_id": persistent_id}, browser_delete_params.BrowserDeleteParams + ), + ), + cast_to=NoneType, + ) + + async def delete_by_id( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """ + Delete Browser Session by ID + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + f"/browsers/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + class BrowsersResourceWithRawResponse: def __init__(self, browsers: BrowsersResource) -> None: @@ -226,6 +412,15 @@ def __init__(self, browsers: BrowsersResource) -> None: self.retrieve = to_raw_response_wrapper( browsers.retrieve, ) + self.list = to_raw_response_wrapper( + browsers.list, + ) + self.delete = to_raw_response_wrapper( + browsers.delete, + ) + self.delete_by_id = to_raw_response_wrapper( + browsers.delete_by_id, + ) class AsyncBrowsersResourceWithRawResponse: @@ -238,6 +433,15 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None: self.retrieve = async_to_raw_response_wrapper( browsers.retrieve, ) + self.list = async_to_raw_response_wrapper( + browsers.list, + ) + self.delete = async_to_raw_response_wrapper( + browsers.delete, + ) + self.delete_by_id = async_to_raw_response_wrapper( + browsers.delete_by_id, + ) class BrowsersResourceWithStreamingResponse: @@ -250,6 +454,15 @@ def __init__(self, browsers: BrowsersResource) -> None: self.retrieve = to_streamed_response_wrapper( browsers.retrieve, ) + self.list = to_streamed_response_wrapper( + browsers.list, + ) + self.delete = to_streamed_response_wrapper( + browsers.delete, + ) + self.delete_by_id = to_streamed_response_wrapper( + browsers.delete_by_id, + ) class AsyncBrowsersResourceWithStreamingResponse: @@ -262,3 +475,12 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None: self.retrieve = async_to_streamed_response_wrapper( browsers.retrieve, ) + self.list = async_to_streamed_response_wrapper( + browsers.list, + ) + self.delete = async_to_streamed_response_wrapper( + browsers.delete, + ) + self.delete_by_id = async_to_streamed_response_wrapper( + browsers.delete_by_id, + ) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index e7c3cecd..d6ca9554 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -4,6 +4,10 @@ from .app_list_params import AppListParams as AppListParams from .app_list_response import AppListResponse as AppListResponse +from .browser_persistence import BrowserPersistence as BrowserPersistence from .browser_create_params import BrowserCreateParams as BrowserCreateParams +from .browser_delete_params import BrowserDeleteParams as BrowserDeleteParams +from .browser_list_response import BrowserListResponse as BrowserListResponse from .browser_create_response import BrowserCreateResponse as BrowserCreateResponse +from .browser_persistence_param import BrowserPersistenceParam as BrowserPersistenceParam from .browser_retrieve_response import BrowserRetrieveResponse as BrowserRetrieveResponse diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index e1f90473..14fd5fe3 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -4,17 +4,14 @@ from typing_extensions import Required, TypedDict -__all__ = ["BrowserCreateParams", "Persistence"] +from .browser_persistence_param import BrowserPersistenceParam + +__all__ = ["BrowserCreateParams"] class BrowserCreateParams(TypedDict, total=False): invocation_id: Required[str] """action invocation ID""" - persistence: Persistence + persistence: BrowserPersistenceParam """Optional persistence configuration for the browser session.""" - - -class Persistence(TypedDict, total=False): - id: Required[str] - """Unique identifier for the persistent browser session.""" diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index a992ef65..f44f3360 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -3,13 +3,9 @@ from typing import Optional from .._models import BaseModel +from .browser_persistence import BrowserPersistence -__all__ = ["BrowserCreateResponse", "Persistence"] - - -class Persistence(BaseModel): - id: str - """Unique identifier for the persistent browser session.""" +__all__ = ["BrowserCreateResponse"] class BrowserCreateResponse(BaseModel): @@ -22,5 +18,5 @@ class BrowserCreateResponse(BaseModel): session_id: str """Unique identifier for the browser session""" - persistence: Optional[Persistence] = None + persistence: Optional[BrowserPersistence] = None """Optional persistence configuration for the browser session.""" diff --git a/src/kernel/types/browser_delete_params.py b/src/kernel/types/browser_delete_params.py new file mode 100644 index 00000000..4c5b1c6a --- /dev/null +++ b/src/kernel/types/browser_delete_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["BrowserDeleteParams"] + + +class BrowserDeleteParams(TypedDict, total=False): + persistent_id: Required[str] + """Persistent browser identifier""" diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py new file mode 100644 index 00000000..d3e90d53 --- /dev/null +++ b/src/kernel/types/browser_list_response.py @@ -0,0 +1,26 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import TypeAlias + +from .._models import BaseModel +from .browser_persistence import BrowserPersistence + +__all__ = ["BrowserListResponse", "BrowserListResponseItem"] + + +class BrowserListResponseItem(BaseModel): + browser_live_view_url: str + """Remote URL for live viewing the browser session""" + + cdp_ws_url: str + """Websocket URL for Chrome DevTools Protocol connections to the browser session""" + + session_id: str + """Unique identifier for the browser session""" + + persistence: Optional[BrowserPersistence] = None + """Optional persistence configuration for the browser session.""" + + +BrowserListResponse: TypeAlias = List[BrowserListResponseItem] diff --git a/src/kernel/types/browser_persistence.py b/src/kernel/types/browser_persistence.py new file mode 100644 index 00000000..9c6bfc7f --- /dev/null +++ b/src/kernel/types/browser_persistence.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["BrowserPersistence"] + + +class BrowserPersistence(BaseModel): + id: str + """Unique identifier for the persistent browser session.""" diff --git a/src/kernel/types/browser_persistence_param.py b/src/kernel/types/browser_persistence_param.py new file mode 100644 index 00000000..b4832918 --- /dev/null +++ b/src/kernel/types/browser_persistence_param.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["BrowserPersistenceParam"] + + +class BrowserPersistenceParam(TypedDict, total=False): + id: Required[str] + """Unique identifier for the persistent browser session.""" diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 7dd69e62..8676b532 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -3,13 +3,9 @@ from typing import Optional from .._models import BaseModel +from .browser_persistence import BrowserPersistence -__all__ = ["BrowserRetrieveResponse", "Persistence"] - - -class Persistence(BaseModel): - id: str - """Unique identifier for the persistent browser session.""" +__all__ = ["BrowserRetrieveResponse"] class BrowserRetrieveResponse(BaseModel): @@ -22,5 +18,5 @@ class BrowserRetrieveResponse(BaseModel): session_id: str """Unique identifier for the browser session""" - persistence: Optional[Persistence] = None + persistence: Optional[BrowserPersistence] = None """Optional persistence configuration for the browser session.""" diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index d4d7a078..ae2faf5f 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -9,7 +9,11 @@ from kernel import Kernel, AsyncKernel from tests.utils import assert_matches_type -from kernel.types import BrowserCreateResponse, BrowserRetrieveResponse +from kernel.types import ( + BrowserListResponse, + BrowserCreateResponse, + BrowserRetrieveResponse, +) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -102,6 +106,110 @@ def test_path_params_retrieve(self, client: Kernel) -> None: "", ) + @pytest.mark.skip() + @parametrize + def test_method_list(self, client: Kernel) -> None: + browser = client.browsers.list() + assert_matches_type(BrowserListResponse, browser, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_raw_response_list(self, client: Kernel) -> None: + response = client.browsers.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser = response.parse() + assert_matches_type(BrowserListResponse, browser, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_streaming_response_list(self, client: Kernel) -> None: + with client.browsers.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser = response.parse() + assert_matches_type(BrowserListResponse, browser, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + def test_method_delete(self, client: Kernel) -> None: + browser = client.browsers.delete( + persistent_id="persistent_id", + ) + assert browser is None + + @pytest.mark.skip() + @parametrize + def test_raw_response_delete(self, client: Kernel) -> None: + response = client.browsers.with_raw_response.delete( + persistent_id="persistent_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser = response.parse() + assert browser is None + + @pytest.mark.skip() + @parametrize + def test_streaming_response_delete(self, client: Kernel) -> None: + with client.browsers.with_streaming_response.delete( + persistent_id="persistent_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser = response.parse() + assert browser is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + def test_method_delete_by_id(self, client: Kernel) -> None: + browser = client.browsers.delete_by_id( + "id", + ) + assert browser is None + + @pytest.mark.skip() + @parametrize + def test_raw_response_delete_by_id(self, client: Kernel) -> None: + response = client.browsers.with_raw_response.delete_by_id( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser = response.parse() + assert browser is None + + @pytest.mark.skip() + @parametrize + def test_streaming_response_delete_by_id(self, client: Kernel) -> None: + with client.browsers.with_streaming_response.delete_by_id( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser = response.parse() + assert browser is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + def test_path_params_delete_by_id(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.with_raw_response.delete_by_id( + "", + ) + class TestAsyncBrowsers: parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) @@ -190,3 +298,107 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: await async_client.browsers.with_raw_response.retrieve( "", ) + + @pytest.mark.skip() + @parametrize + async def test_method_list(self, async_client: AsyncKernel) -> None: + browser = await async_client.browsers.list() + assert_matches_type(BrowserListResponse, browser, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_raw_response_list(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser = await response.parse() + assert_matches_type(BrowserListResponse, browser, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser = await response.parse() + assert_matches_type(BrowserListResponse, browser, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + async def test_method_delete(self, async_client: AsyncKernel) -> None: + browser = await async_client.browsers.delete( + persistent_id="persistent_id", + ) + assert browser is None + + @pytest.mark.skip() + @parametrize + async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.with_raw_response.delete( + persistent_id="persistent_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser = await response.parse() + assert browser is None + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.with_streaming_response.delete( + persistent_id="persistent_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser = await response.parse() + assert browser is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + async def test_method_delete_by_id(self, async_client: AsyncKernel) -> None: + browser = await async_client.browsers.delete_by_id( + "id", + ) + assert browser is None + + @pytest.mark.skip() + @parametrize + async def test_raw_response_delete_by_id(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.with_raw_response.delete_by_id( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser = await response.parse() + assert browser is None + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_delete_by_id(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.with_streaming_response.delete_by_id( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser = await response.parse() + assert browser is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + async def test_path_params_delete_by_id(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.with_raw_response.delete_by_id( + "", + ) From f7da56fcd87645a4aaa57dc4f37ccb40b3e3630a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 16:51:07 +0000 Subject: [PATCH 057/448] feat(api): update via SDK Studio --- .stats.yml | 4 ++-- README.md | 4 ++-- tests/api_resources/test_browsers.py | 20 ++++++++++---------- tests/test_client.py | 12 ++++++------ 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/.stats.yml b/.stats.yml index 77c3857b..c728bcd7 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 10 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-ffefc234d11c041cadab66fa6e7c379cebbd9422d38f2b1b1019e425ae19bbd8.yml -openapi_spec_hash: aa04a371ff95b44847450d657ad0a920 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-3edc7a0eef4a0d4495782efbdb0d9b777a55aee058dab119f90de56019441326.yml +openapi_spec_hash: dff0b1efa1c1614cf770ed8327cefab2 config_hash: f33cc77a9c01e879ad127194c897a988 diff --git a/README.md b/README.md index 28474ed5..a940dbb4 100644 --- a/README.md +++ b/README.md @@ -97,8 +97,8 @@ from kernel import Kernel client = Kernel() browser = client.browsers.create( - invocation_id="ckqwer3o20000jb9s7abcdef", - persistence={"id": "my-shared-browser"}, + invocation_id="rr33xuugxj9h0bkf1rdt2bet", + persistence={"id": "my-awesome-browser-for-user-1234"}, ) print(browser.persistence) ``` diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index ae2faf5f..260d1719 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -25,7 +25,7 @@ class TestBrowsers: @parametrize def test_method_create(self, client: Kernel) -> None: browser = client.browsers.create( - invocation_id="ckqwer3o20000jb9s7abcdef", + invocation_id="rr33xuugxj9h0bkf1rdt2bet", ) assert_matches_type(BrowserCreateResponse, browser, path=["response"]) @@ -33,8 +33,8 @@ def test_method_create(self, client: Kernel) -> None: @parametrize def test_method_create_with_all_params(self, client: Kernel) -> None: browser = client.browsers.create( - invocation_id="ckqwer3o20000jb9s7abcdef", - persistence={"id": "my-shared-browser"}, + invocation_id="rr33xuugxj9h0bkf1rdt2bet", + persistence={"id": "my-awesome-browser-for-user-1234"}, ) assert_matches_type(BrowserCreateResponse, browser, path=["response"]) @@ -42,7 +42,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: @parametrize def test_raw_response_create(self, client: Kernel) -> None: response = client.browsers.with_raw_response.create( - invocation_id="ckqwer3o20000jb9s7abcdef", + invocation_id="rr33xuugxj9h0bkf1rdt2bet", ) assert response.is_closed is True @@ -54,7 +54,7 @@ def test_raw_response_create(self, client: Kernel) -> None: @parametrize def test_streaming_response_create(self, client: Kernel) -> None: with client.browsers.with_streaming_response.create( - invocation_id="ckqwer3o20000jb9s7abcdef", + invocation_id="rr33xuugxj9h0bkf1rdt2bet", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -218,7 +218,7 @@ class TestAsyncBrowsers: @parametrize async def test_method_create(self, async_client: AsyncKernel) -> None: browser = await async_client.browsers.create( - invocation_id="ckqwer3o20000jb9s7abcdef", + invocation_id="rr33xuugxj9h0bkf1rdt2bet", ) assert_matches_type(BrowserCreateResponse, browser, path=["response"]) @@ -226,8 +226,8 @@ async def test_method_create(self, async_client: AsyncKernel) -> None: @parametrize async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: browser = await async_client.browsers.create( - invocation_id="ckqwer3o20000jb9s7abcdef", - persistence={"id": "my-shared-browser"}, + invocation_id="rr33xuugxj9h0bkf1rdt2bet", + persistence={"id": "my-awesome-browser-for-user-1234"}, ) assert_matches_type(BrowserCreateResponse, browser, path=["response"]) @@ -235,7 +235,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> @parametrize async def test_raw_response_create(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.with_raw_response.create( - invocation_id="ckqwer3o20000jb9s7abcdef", + invocation_id="rr33xuugxj9h0bkf1rdt2bet", ) assert response.is_closed is True @@ -247,7 +247,7 @@ async def test_raw_response_create(self, async_client: AsyncKernel) -> None: @parametrize async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: async with async_client.browsers.with_streaming_response.create( - invocation_id="ckqwer3o20000jb9s7abcdef", + invocation_id="rr33xuugxj9h0bkf1rdt2bet", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/test_client.py b/tests/test_client.py index 00d7a014..be1e7a10 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -768,7 +768,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/browsers").mock(side_effect=retry_handler) - response = client.browsers.with_raw_response.create(invocation_id="ckqwer3o20000jb9s7abcdef") + response = client.browsers.with_raw_response.create(invocation_id="rr33xuugxj9h0bkf1rdt2bet") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -793,7 +793,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/browsers").mock(side_effect=retry_handler) response = client.browsers.with_raw_response.create( - invocation_id="ckqwer3o20000jb9s7abcdef", extra_headers={"x-stainless-retry-count": Omit()} + invocation_id="rr33xuugxj9h0bkf1rdt2bet", extra_headers={"x-stainless-retry-count": Omit()} ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -818,7 +818,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/browsers").mock(side_effect=retry_handler) response = client.browsers.with_raw_response.create( - invocation_id="ckqwer3o20000jb9s7abcdef", extra_headers={"x-stainless-retry-count": "42"} + invocation_id="rr33xuugxj9h0bkf1rdt2bet", extra_headers={"x-stainless-retry-count": "42"} ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1559,7 +1559,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/browsers").mock(side_effect=retry_handler) - response = await client.browsers.with_raw_response.create(invocation_id="ckqwer3o20000jb9s7abcdef") + response = await client.browsers.with_raw_response.create(invocation_id="rr33xuugxj9h0bkf1rdt2bet") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1585,7 +1585,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/browsers").mock(side_effect=retry_handler) response = await client.browsers.with_raw_response.create( - invocation_id="ckqwer3o20000jb9s7abcdef", extra_headers={"x-stainless-retry-count": Omit()} + invocation_id="rr33xuugxj9h0bkf1rdt2bet", extra_headers={"x-stainless-retry-count": Omit()} ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -1611,7 +1611,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/browsers").mock(side_effect=retry_handler) response = await client.browsers.with_raw_response.create( - invocation_id="ckqwer3o20000jb9s7abcdef", extra_headers={"x-stainless-retry-count": "42"} + invocation_id="rr33xuugxj9h0bkf1rdt2bet", extra_headers={"x-stainless-retry-count": "42"} ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" From 43f0dbbbd0308043bc61a2a5009e6e1858269cd2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 16:54:01 +0000 Subject: [PATCH 058/448] feat(api): update via SDK Studio --- .stats.yml | 2 +- README.md | 7 +++++++ tests/test_client.py | 32 ++++++++++++++++++++++++++++---- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/.stats.yml b/.stats.yml index c728bcd7..d4463c35 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 10 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-3edc7a0eef4a0d4495782efbdb0d9b777a55aee058dab119f90de56019441326.yml openapi_spec_hash: dff0b1efa1c1614cf770ed8327cefab2 -config_hash: f33cc77a9c01e879ad127194c897a988 +config_hash: cb04a4d88ee9f530b303ca57ff7090b3 diff --git a/README.md b/README.md index a940dbb4..9d3c4fa3 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,7 @@ client = Kernel() try: client.browsers.create( invocation_id="REPLACE_ME", + persistence={"id": "browser-for-user-1234"}, ) except kernel.APIConnectionError as e: print("The server could not be reached") @@ -184,6 +185,7 @@ client = Kernel( # Or, configure per-request: client.with_options(max_retries=5).browsers.create( invocation_id="REPLACE_ME", + persistence={"id": "browser-for-user-1234"}, ) ``` @@ -209,6 +211,7 @@ client = Kernel( # Override per-request: client.with_options(timeout=5.0).browsers.create( invocation_id="REPLACE_ME", + persistence={"id": "browser-for-user-1234"}, ) ``` @@ -252,6 +255,9 @@ from kernel import Kernel client = Kernel() response = client.browsers.with_raw_response.create( invocation_id="REPLACE_ME", + persistence={ + "id": "browser-for-user-1234" + }, ) print(response.headers.get('X-My-Header')) @@ -272,6 +278,7 @@ To stream the response body, use `.with_streaming_response` instead, which requi ```python with client.browsers.with_streaming_response.create( invocation_id="REPLACE_ME", + persistence={"id": "browser-for-user-1234"}, ) as response: print(response.headers.get("X-My-Header")) diff --git a/tests/test_client.py b/tests/test_client.py index be1e7a10..38e2d56a 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -720,7 +720,13 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No with pytest.raises(APITimeoutError): self.client.post( "/browsers", - body=cast(object, maybe_transform(dict(invocation_id="REPLACE_ME"), BrowserCreateParams)), + body=cast( + object, + maybe_transform( + dict(invocation_id="REPLACE_ME", persistence={"id": "browser-for-user-1234"}), + BrowserCreateParams, + ), + ), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -735,7 +741,13 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non with pytest.raises(APIStatusError): self.client.post( "/browsers", - body=cast(object, maybe_transform(dict(invocation_id="REPLACE_ME"), BrowserCreateParams)), + body=cast( + object, + maybe_transform( + dict(invocation_id="REPLACE_ME", persistence={"id": "browser-for-user-1234"}), + BrowserCreateParams, + ), + ), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -1510,7 +1522,13 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) with pytest.raises(APITimeoutError): await self.client.post( "/browsers", - body=cast(object, maybe_transform(dict(invocation_id="REPLACE_ME"), BrowserCreateParams)), + body=cast( + object, + maybe_transform( + dict(invocation_id="REPLACE_ME", persistence={"id": "browser-for-user-1234"}), + BrowserCreateParams, + ), + ), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -1525,7 +1543,13 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) with pytest.raises(APIStatusError): await self.client.post( "/browsers", - body=cast(object, maybe_transform(dict(invocation_id="REPLACE_ME"), BrowserCreateParams)), + body=cast( + object, + maybe_transform( + dict(invocation_id="REPLACE_ME", persistence={"id": "browser-for-user-1234"}), + BrowserCreateParams, + ), + ), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) From f1a1afc91375734952de93f2e5507e00439e94e0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 20:24:18 +0000 Subject: [PATCH 059/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 10f30916..6b7b74c5 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.2.0" + ".": "0.3.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 034e1164..d4bf1e30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.2.0" +version = "0.3.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 4f726fa3..bd4f5195 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.2.0" # x-release-please-version +__version__ = "0.3.0" # x-release-please-version From ffb27b70328e5b11b1cdf446a6acf4b266622563 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 28 May 2025 02:11:13 +0000 Subject: [PATCH 060/448] feat(api): update via SDK Studio --- .stats.yml | 8 +- api.md | 7 +- src/kernel/resources/apps/invocations.py | 119 ++++++++++++++++- src/kernel/types/apps/__init__.py | 2 + .../types/apps/invocation_create_params.py | 10 +- .../types/apps/invocation_update_params.py | 15 +++ .../types/apps/invocation_update_response.py | 47 +++++++ tests/api_resources/apps/test_invocations.py | 120 +++++++++++++++++- 8 files changed, 320 insertions(+), 8 deletions(-) create mode 100644 src/kernel/types/apps/invocation_update_params.py create mode 100644 src/kernel/types/apps/invocation_update_response.py diff --git a/.stats.yml b/.stats.yml index d4463c35..be606c6d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 10 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-3edc7a0eef4a0d4495782efbdb0d9b777a55aee058dab119f90de56019441326.yml -openapi_spec_hash: dff0b1efa1c1614cf770ed8327cefab2 -config_hash: cb04a4d88ee9f530b303ca57ff7090b3 +configured_endpoints: 11 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-64ccdff4ca5d73d79d89e817fe83ccfd3d529696df3e6818c3c75e586ae00801.yml +openapi_spec_hash: 21c7b8757fc0cc9415cda1bc06251de6 +config_hash: b3fcacd707da56b21d31ce0baf4fb87d diff --git a/api.md b/api.md index e94025bc..cbacba5d 100644 --- a/api.md +++ b/api.md @@ -28,13 +28,18 @@ Methods: Types: ```python -from kernel.types.apps import InvocationCreateResponse, InvocationRetrieveResponse +from kernel.types.apps import ( + InvocationCreateResponse, + InvocationRetrieveResponse, + InvocationUpdateResponse, +) ``` Methods: - client.apps.invocations.create(\*\*params) -> InvocationCreateResponse - client.apps.invocations.retrieve(id) -> InvocationRetrieveResponse +- client.apps.invocations.update(id, \*\*params) -> InvocationUpdateResponse # Browsers diff --git a/src/kernel/resources/apps/invocations.py b/src/kernel/resources/apps/invocations.py index 44015013..3f1f495a 100644 --- a/src/kernel/resources/apps/invocations.py +++ b/src/kernel/resources/apps/invocations.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing_extensions import Literal + import httpx from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven @@ -14,9 +16,10 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from ...types.apps import invocation_create_params +from ...types.apps import invocation_create_params, invocation_update_params from ..._base_client import make_request_options from ...types.apps.invocation_create_response import InvocationCreateResponse +from ...types.apps.invocation_update_response import InvocationUpdateResponse from ...types.apps.invocation_retrieve_response import InvocationRetrieveResponse __all__ = ["InvocationsResource", "AsyncInvocationsResource"] @@ -48,6 +51,7 @@ def create( action_name: str, app_name: str, version: str, + async_: bool | NotGiven = NOT_GIVEN, payload: 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. @@ -66,6 +70,9 @@ def create( version: Version of the application + async_: If true, invoke asynchronously. When set, the API responds 202 Accepted with + status "queued". + payload: Input data for the action, sent as a JSON string. extra_headers: Send extra headers @@ -83,6 +90,7 @@ def create( "action_name": action_name, "app_name": app_name, "version": version, + "async_": async_, "payload": payload, }, invocation_create_params.InvocationCreateParams, @@ -126,6 +134,52 @@ def retrieve( cast_to=InvocationRetrieveResponse, ) + def update( + self, + id: str, + *, + status: Literal["succeeded", "failed"], + output: 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, + ) -> InvocationUpdateResponse: + """ + Update invocation status or output + + Args: + status: New status for the invocation. + + output: Updated output of the invocation rendered as JSON string. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._patch( + f"/invocations/{id}", + body=maybe_transform( + { + "status": status, + "output": output, + }, + invocation_update_params.InvocationUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=InvocationUpdateResponse, + ) + class AsyncInvocationsResource(AsyncAPIResource): @cached_property @@ -153,6 +207,7 @@ async def create( action_name: str, app_name: str, version: str, + async_: bool | NotGiven = NOT_GIVEN, payload: 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. @@ -171,6 +226,9 @@ async def create( version: Version of the application + async_: If true, invoke asynchronously. When set, the API responds 202 Accepted with + status "queued". + payload: Input data for the action, sent as a JSON string. extra_headers: Send extra headers @@ -188,6 +246,7 @@ async def create( "action_name": action_name, "app_name": app_name, "version": version, + "async_": async_, "payload": payload, }, invocation_create_params.InvocationCreateParams, @@ -231,6 +290,52 @@ async def retrieve( cast_to=InvocationRetrieveResponse, ) + async def update( + self, + id: str, + *, + status: Literal["succeeded", "failed"], + output: 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, + ) -> InvocationUpdateResponse: + """ + Update invocation status or output + + Args: + status: New status for the invocation. + + output: Updated output of the invocation rendered as JSON string. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._patch( + f"/invocations/{id}", + body=await async_maybe_transform( + { + "status": status, + "output": output, + }, + invocation_update_params.InvocationUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=InvocationUpdateResponse, + ) + class InvocationsResourceWithRawResponse: def __init__(self, invocations: InvocationsResource) -> None: @@ -242,6 +347,9 @@ def __init__(self, invocations: InvocationsResource) -> None: self.retrieve = to_raw_response_wrapper( invocations.retrieve, ) + self.update = to_raw_response_wrapper( + invocations.update, + ) class AsyncInvocationsResourceWithRawResponse: @@ -254,6 +362,9 @@ def __init__(self, invocations: AsyncInvocationsResource) -> None: self.retrieve = async_to_raw_response_wrapper( invocations.retrieve, ) + self.update = async_to_raw_response_wrapper( + invocations.update, + ) class InvocationsResourceWithStreamingResponse: @@ -266,6 +377,9 @@ def __init__(self, invocations: InvocationsResource) -> None: self.retrieve = to_streamed_response_wrapper( invocations.retrieve, ) + self.update = to_streamed_response_wrapper( + invocations.update, + ) class AsyncInvocationsResourceWithStreamingResponse: @@ -278,3 +392,6 @@ def __init__(self, invocations: AsyncInvocationsResource) -> None: self.retrieve = async_to_streamed_response_wrapper( invocations.retrieve, ) + self.update = async_to_streamed_response_wrapper( + invocations.update, + ) diff --git a/src/kernel/types/apps/__init__.py b/src/kernel/types/apps/__init__.py index 425fffd4..f4bf7a25 100644 --- a/src/kernel/types/apps/__init__.py +++ b/src/kernel/types/apps/__init__.py @@ -4,7 +4,9 @@ from .deployment_create_params import DeploymentCreateParams as DeploymentCreateParams from .invocation_create_params import InvocationCreateParams as InvocationCreateParams +from .invocation_update_params import InvocationUpdateParams as InvocationUpdateParams from .deployment_create_response import DeploymentCreateResponse as DeploymentCreateResponse from .deployment_follow_response import DeploymentFollowResponse as DeploymentFollowResponse from .invocation_create_response import InvocationCreateResponse as InvocationCreateResponse +from .invocation_update_response import InvocationUpdateResponse as InvocationUpdateResponse from .invocation_retrieve_response import InvocationRetrieveResponse as InvocationRetrieveResponse diff --git a/src/kernel/types/apps/invocation_create_params.py b/src/kernel/types/apps/invocation_create_params.py index a97a2c5a..01035ae5 100644 --- a/src/kernel/types/apps/invocation_create_params.py +++ b/src/kernel/types/apps/invocation_create_params.py @@ -2,7 +2,9 @@ from __future__ import annotations -from typing_extensions import Required, TypedDict +from typing_extensions import Required, Annotated, TypedDict + +from ..._utils import PropertyInfo __all__ = ["InvocationCreateParams"] @@ -17,5 +19,11 @@ class InvocationCreateParams(TypedDict, total=False): version: Required[str] """Version of the application""" + async_: Annotated[bool, PropertyInfo(alias="async")] + """If true, invoke asynchronously. + + When set, the API responds 202 Accepted with status "queued". + """ + payload: str """Input data for the action, sent as a JSON string.""" diff --git a/src/kernel/types/apps/invocation_update_params.py b/src/kernel/types/apps/invocation_update_params.py new file mode 100644 index 00000000..72ccf5d9 --- /dev/null +++ b/src/kernel/types/apps/invocation_update_params.py @@ -0,0 +1,15 @@ +# 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__ = ["InvocationUpdateParams"] + + +class InvocationUpdateParams(TypedDict, total=False): + status: Required[Literal["succeeded", "failed"]] + """New status for the invocation.""" + + output: str + """Updated output of the invocation rendered as JSON string.""" diff --git a/src/kernel/types/apps/invocation_update_response.py b/src/kernel/types/apps/invocation_update_response.py new file mode 100644 index 00000000..c30fc160 --- /dev/null +++ b/src/kernel/types/apps/invocation_update_response.py @@ -0,0 +1,47 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["InvocationUpdateResponse"] + + +class InvocationUpdateResponse(BaseModel): + id: str + """ID of the invocation""" + + action_name: str + """Name of the action invoked""" + + app_name: str + """Name of the application""" + + started_at: datetime + """RFC 3339 Nanoseconds timestamp when the invocation started""" + + status: Literal["queued", "running", "succeeded", "failed"] + """Status of the invocation""" + + finished_at: Optional[datetime] = None + """ + RFC 3339 Nanoseconds timestamp when the invocation finished (null if still + running) + """ + + output: Optional[str] = None + """Output produced by the action, rendered as a JSON string. + + This could be: string, number, boolean, array, object, or null. + """ + + payload: Optional[str] = None + """Payload provided to the invocation. + + This is a string that can be parsed as JSON. + """ + + status_reason: Optional[str] = None + """Status reason""" diff --git a/tests/api_resources/apps/test_invocations.py b/tests/api_resources/apps/test_invocations.py index 61af0316..87dc31fc 100644 --- a/tests/api_resources/apps/test_invocations.py +++ b/tests/api_resources/apps/test_invocations.py @@ -9,7 +9,11 @@ from kernel import Kernel, AsyncKernel from tests.utils import assert_matches_type -from kernel.types.apps import InvocationCreateResponse, InvocationRetrieveResponse +from kernel.types.apps import ( + InvocationCreateResponse, + InvocationUpdateResponse, + InvocationRetrieveResponse, +) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -34,6 +38,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: action_name="analyze", app_name="my-app", version="1.0.0", + async_=True, payload='{"data":"example input"}', ) assert_matches_type(InvocationCreateResponse, invocation, path=["response"]) @@ -110,6 +115,62 @@ def test_path_params_retrieve(self, client: Kernel) -> None: "", ) + @pytest.mark.skip() + @parametrize + def test_method_update(self, client: Kernel) -> None: + invocation = client.apps.invocations.update( + id="id", + status="succeeded", + ) + assert_matches_type(InvocationUpdateResponse, invocation, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_method_update_with_all_params(self, client: Kernel) -> None: + invocation = client.apps.invocations.update( + id="id", + status="succeeded", + output="output", + ) + assert_matches_type(InvocationUpdateResponse, invocation, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_raw_response_update(self, client: Kernel) -> None: + response = client.apps.invocations.with_raw_response.update( + id="id", + status="succeeded", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + invocation = response.parse() + assert_matches_type(InvocationUpdateResponse, invocation, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_streaming_response_update(self, client: Kernel) -> None: + with client.apps.invocations.with_streaming_response.update( + id="id", + status="succeeded", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + invocation = response.parse() + assert_matches_type(InvocationUpdateResponse, invocation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + def test_path_params_update(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.apps.invocations.with_raw_response.update( + id="", + status="succeeded", + ) + class TestAsyncInvocations: parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) @@ -131,6 +192,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> action_name="analyze", app_name="my-app", version="1.0.0", + async_=True, payload='{"data":"example input"}', ) assert_matches_type(InvocationCreateResponse, invocation, path=["response"]) @@ -206,3 +268,59 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: await async_client.apps.invocations.with_raw_response.retrieve( "", ) + + @pytest.mark.skip() + @parametrize + async def test_method_update(self, async_client: AsyncKernel) -> None: + invocation = await async_client.apps.invocations.update( + id="id", + status="succeeded", + ) + assert_matches_type(InvocationUpdateResponse, invocation, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> None: + invocation = await async_client.apps.invocations.update( + id="id", + status="succeeded", + output="output", + ) + assert_matches_type(InvocationUpdateResponse, invocation, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_raw_response_update(self, async_client: AsyncKernel) -> None: + response = await async_client.apps.invocations.with_raw_response.update( + id="id", + status="succeeded", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + invocation = await response.parse() + assert_matches_type(InvocationUpdateResponse, invocation, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_update(self, async_client: AsyncKernel) -> None: + async with async_client.apps.invocations.with_streaming_response.update( + id="id", + status="succeeded", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + invocation = await response.parse() + assert_matches_type(InvocationUpdateResponse, invocation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + async def test_path_params_update(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.apps.invocations.with_raw_response.update( + id="", + status="succeeded", + ) From 83cc9d785fae8b7f65c7b0874e6eef6cabce8fcc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 28 May 2025 18:45:14 +0000 Subject: [PATCH 061/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 6b7b74c5..da59f99e 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.3.0" + ".": "0.4.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index d4bf1e30..ed0ec2d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.3.0" +version = "0.4.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index bd4f5195..e2017455 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.3.0" # x-release-please-version +__version__ = "0.4.0" # x-release-please-version From 440f3015c4360fa1be2fa7e2de1471ab0858d5de Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 30 May 2025 12:30:10 +0000 Subject: [PATCH 062/448] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index be606c6d..449c1d2c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 11 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-64ccdff4ca5d73d79d89e817fe83ccfd3d529696df3e6818c3c75e586ae00801.yml openapi_spec_hash: 21c7b8757fc0cc9415cda1bc06251de6 -config_hash: b3fcacd707da56b21d31ce0baf4fb87d +config_hash: f03f4ba5576f016fbd430540e2e78804 From adfead20c81eebb40d889b3887b3da35a80f4528 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 30 May 2025 13:51:54 +0000 Subject: [PATCH 063/448] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 449c1d2c..928da34d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 11 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-64ccdff4ca5d73d79d89e817fe83ccfd3d529696df3e6818c3c75e586ae00801.yml openapi_spec_hash: 21c7b8757fc0cc9415cda1bc06251de6 -config_hash: f03f4ba5576f016fbd430540e2e78804 +config_hash: 4bc202cdd2df5cd211fa97e999498052 From 65d55eee5306184062d746cd08c0183b8f2bc5f0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 30 May 2025 13:52:23 +0000 Subject: [PATCH 064/448] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 928da34d..cd344247 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 11 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-64ccdff4ca5d73d79d89e817fe83ccfd3d529696df3e6818c3c75e586ae00801.yml openapi_spec_hash: 21c7b8757fc0cc9415cda1bc06251de6 -config_hash: 4bc202cdd2df5cd211fa97e999498052 +config_hash: c6bab7ac8da570a5abbcfb19db119b6b From 681a0b03ba59b182971a4050d439bbd811188fca Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 30 May 2025 14:39:00 +0000 Subject: [PATCH 065/448] feat(api): update via SDK Studio --- .stats.yml | 4 ++-- src/kernel/resources/apps/apps.py | 10 ++++------ src/kernel/resources/apps/deployments.py | 4 ++-- src/kernel/resources/apps/invocations.py | 12 ++++++------ src/kernel/resources/browsers.py | 20 ++++++++++---------- 5 files changed, 24 insertions(+), 26 deletions(-) diff --git a/.stats.yml b/.stats.yml index cd344247..dbb369ac 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 11 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-64ccdff4ca5d73d79d89e817fe83ccfd3d529696df3e6818c3c75e586ae00801.yml -openapi_spec_hash: 21c7b8757fc0cc9415cda1bc06251de6 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-1f7397b87108a992979b665f45bf0aee5b10387e8124f4768c4c7852ba0b23d7.yml +openapi_spec_hash: e5460337788e7eab0d8f05ef2f55086e config_hash: c6bab7ac8da570a5abbcfb19db119b6b diff --git a/src/kernel/resources/apps/apps.py b/src/kernel/resources/apps/apps.py index 9a5f6670..3769bd57 100644 --- a/src/kernel/resources/apps/apps.py +++ b/src/kernel/resources/apps/apps.py @@ -77,10 +77,9 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> AppListResponse: - """List application versions for the authenticated user. + """List applications. - Optionally filter by app - name and/or version label. + Optionally filter by app name and/or version label. Args: app_name: Filter results by application name. @@ -154,10 +153,9 @@ async def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> AppListResponse: - """List application versions for the authenticated user. + """List applications. - Optionally filter by app - name and/or version label. + Optionally filter by app name and/or version label. Args: app_name: Filter results by application name. diff --git a/src/kernel/resources/apps/deployments.py b/src/kernel/resources/apps/deployments.py index a3e364aa..98d1728c 100644 --- a/src/kernel/resources/apps/deployments.py +++ b/src/kernel/resources/apps/deployments.py @@ -63,7 +63,7 @@ def create( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> DeploymentCreateResponse: """ - Deploy a new application + Deploy a new application and associated actions to Kernel. Args: entrypoint_rel_path: Relative path to the entrypoint of the application @@ -190,7 +190,7 @@ async def create( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> DeploymentCreateResponse: """ - Deploy a new application + Deploy a new application and associated actions to Kernel. Args: entrypoint_rel_path: Relative path to the entrypoint of the application diff --git a/src/kernel/resources/apps/invocations.py b/src/kernel/resources/apps/invocations.py index 3f1f495a..b5413d4f 100644 --- a/src/kernel/resources/apps/invocations.py +++ b/src/kernel/resources/apps/invocations.py @@ -61,7 +61,7 @@ def create( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> InvocationCreateResponse: """ - Invoke an application + Invoke an action. Args: action_name: Name of the action to invoke @@ -113,7 +113,7 @@ def retrieve( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> InvocationRetrieveResponse: """ - Get an app invocation by id + Get details about an invocation's status and output. Args: extra_headers: Send extra headers @@ -148,7 +148,7 @@ def update( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> InvocationUpdateResponse: """ - Update invocation status or output + Update an invocation's status or output. Args: status: New status for the invocation. @@ -217,7 +217,7 @@ async def create( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> InvocationCreateResponse: """ - Invoke an application + Invoke an action. Args: action_name: Name of the action to invoke @@ -269,7 +269,7 @@ async def retrieve( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> InvocationRetrieveResponse: """ - Get an app invocation by id + Get details about an invocation's status and output. Args: extra_headers: Send extra headers @@ -304,7 +304,7 @@ async def update( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> InvocationUpdateResponse: """ - Update invocation status or output + Update an invocation's status or output. Args: status: New status for the invocation. diff --git a/src/kernel/resources/browsers.py b/src/kernel/resources/browsers.py index 6816edd6..375723ab 100644 --- a/src/kernel/resources/browsers.py +++ b/src/kernel/resources/browsers.py @@ -57,7 +57,7 @@ def create( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> BrowserCreateResponse: """ - Create Browser Session + Create a new browser session from within an action. Args: invocation_id: action invocation ID @@ -99,7 +99,7 @@ def retrieve( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> BrowserRetrieveResponse: """ - Get Browser Session by ID + Get information about a browser session. Args: extra_headers: Send extra headers @@ -130,7 +130,7 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> BrowserListResponse: - """List active browser sessions for the authenticated user""" + """List active browser sessions""" return self._get( "/browsers", options=make_request_options( @@ -151,7 +151,7 @@ def delete( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> None: """ - Delete a persistent browser session by persistent_id query parameter. + Delete a persistent browser session by its persistent_id. Args: persistent_id: Persistent browser identifier @@ -189,7 +189,7 @@ def delete_by_id( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> None: """ - Delete Browser Session by ID + Delete a browser session by ID Args: extra_headers: Send extra headers @@ -245,7 +245,7 @@ async def create( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> BrowserCreateResponse: """ - Create Browser Session + Create a new browser session from within an action. Args: invocation_id: action invocation ID @@ -287,7 +287,7 @@ async def retrieve( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> BrowserRetrieveResponse: """ - Get Browser Session by ID + Get information about a browser session. Args: extra_headers: Send extra headers @@ -318,7 +318,7 @@ async def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> BrowserListResponse: - """List active browser sessions for the authenticated user""" + """List active browser sessions""" return await self._get( "/browsers", options=make_request_options( @@ -339,7 +339,7 @@ async def delete( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> None: """ - Delete a persistent browser session by persistent_id query parameter. + Delete a persistent browser session by its persistent_id. Args: persistent_id: Persistent browser identifier @@ -379,7 +379,7 @@ async def delete_by_id( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> None: """ - Delete Browser Session by ID + Delete a browser session by ID Args: extra_headers: Send extra headers From f46678e6bdedd4f24a22f470680cc8887878c03e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 30 May 2025 14:43:34 +0000 Subject: [PATCH 066/448] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index dbb369ac..03ed9444 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 11 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-1f7397b87108a992979b665f45bf0aee5b10387e8124f4768c4c7852ba0b23d7.yml -openapi_spec_hash: e5460337788e7eab0d8f05ef2f55086e +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-7af0ef1d19efb9231c098855b72668646401afd5e00400953aca0728f7ceadb7.yml +openapi_spec_hash: fb160fe8ee0cda0a1ce9766c8195ee68 config_hash: c6bab7ac8da570a5abbcfb19db119b6b From dd5a99568ec04f7eacbf38ffbafd3e56a2821ba6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 30 May 2025 17:24:12 +0000 Subject: [PATCH 067/448] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 03ed9444..a9ddb6d5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 11 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-7af0ef1d19efb9231c098855b72668646401afd5e00400953aca0728f7ceadb7.yml -openapi_spec_hash: fb160fe8ee0cda0a1ce9766c8195ee68 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-b91d95f8e40f28d0e455d749b86c4d864ac15a264dcc2c5b317f626ff605ce2c.yml +openapi_spec_hash: befc3a683593ad7d832cfa9f0db941aa config_hash: c6bab7ac8da570a5abbcfb19db119b6b From d9ce83ed076590201b6881bc57233ef23bb6e8fe Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 02:23:57 +0000 Subject: [PATCH 068/448] chore(docs): remove reference to rye shell --- CONTRIBUTING.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c486484d..f05c930b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,8 +17,7 @@ $ rye sync --all-features You can then run scripts using `rye run python script.py` or by activating the virtual environment: ```sh -$ rye shell -# or manually activate - https://docs.python.org/3/library/venv.html#how-venvs-work +# Activate the virtual environment - https://docs.python.org/3/library/venv.html#how-venvs-work $ source .venv/bin/activate # now you can omit the `rye run` prefix From e166730c282a08a96e58d95f736aa7e0ce32e10a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 03:38:40 +0000 Subject: [PATCH 069/448] feat(client): add follow_redirects request option --- src/kernel/_base_client.py | 6 +++++ src/kernel/_models.py | 2 ++ src/kernel/_types.py | 2 ++ tests/test_client.py | 54 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 64 insertions(+) diff --git a/src/kernel/_base_client.py b/src/kernel/_base_client.py index 34308dde..785adea6 100644 --- a/src/kernel/_base_client.py +++ b/src/kernel/_base_client.py @@ -960,6 +960,9 @@ def request( if self.custom_auth is not None: kwargs["auth"] = self.custom_auth + if options.follow_redirects is not None: + kwargs["follow_redirects"] = options.follow_redirects + log.debug("Sending HTTP Request: %s %s", request.method, request.url) response = None @@ -1460,6 +1463,9 @@ async def request( if self.custom_auth is not None: kwargs["auth"] = self.custom_auth + if options.follow_redirects is not None: + kwargs["follow_redirects"] = options.follow_redirects + log.debug("Sending HTTP Request: %s %s", request.method, request.url) response = None diff --git a/src/kernel/_models.py b/src/kernel/_models.py index 798956f1..4f214980 100644 --- a/src/kernel/_models.py +++ b/src/kernel/_models.py @@ -737,6 +737,7 @@ class FinalRequestOptionsInput(TypedDict, total=False): idempotency_key: str json_data: Body extra_json: AnyMapping + follow_redirects: bool @final @@ -750,6 +751,7 @@ class FinalRequestOptions(pydantic.BaseModel): files: Union[HttpxRequestFiles, None] = None idempotency_key: Union[str, None] = None post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() + follow_redirects: Union[bool, None] = None # It should be noted that we cannot use `json` here as that would override # a BaseModel method in an incompatible fashion. diff --git a/src/kernel/_types.py b/src/kernel/_types.py index 2b0c5c3c..18a1ef53 100644 --- a/src/kernel/_types.py +++ b/src/kernel/_types.py @@ -100,6 +100,7 @@ class RequestOptions(TypedDict, total=False): params: Query extra_json: AnyMapping idempotency_key: str + follow_redirects: bool # Sentinel class used until PEP 0661 is accepted @@ -215,3 +216,4 @@ class _GenericAlias(Protocol): class HttpxSendArgs(TypedDict, total=False): auth: httpx.Auth + follow_redirects: bool diff --git a/tests/test_client.py b/tests/test_client.py index 38e2d56a..575e4a4c 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -835,6 +835,33 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: assert response.http_request.headers.get("x-stainless-retry-count") == "42" + @pytest.mark.respx(base_url=base_url) + def test_follow_redirects(self, respx_mock: MockRouter) -> None: + # Test that the default follow_redirects=True allows following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"})) + + response = self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) + assert response.status_code == 200 + assert response.json() == {"status": "ok"} + + @pytest.mark.respx(base_url=base_url) + def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None: + # Test that follow_redirects=False prevents following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + + with pytest.raises(APIStatusError) as exc_info: + self.client.post( + "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response + ) + + assert exc_info.value.response.status_code == 302 + assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected" + class TestAsyncKernel: client = AsyncKernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) @@ -1684,3 +1711,30 @@ async def test_main() -> None: raise AssertionError("calling get_platform using asyncify resulted in a hung process") time.sleep(0.1) + + @pytest.mark.respx(base_url=base_url) + async def test_follow_redirects(self, respx_mock: MockRouter) -> None: + # Test that the default follow_redirects=True allows following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"})) + + response = await self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) + assert response.status_code == 200 + assert response.json() == {"status": "ok"} + + @pytest.mark.respx(base_url=base_url) + async def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None: + # Test that follow_redirects=False prevents following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + + with pytest.raises(APIStatusError) as exc_info: + await self.client.post( + "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response + ) + + assert exc_info.value.response.status_code == 302 + assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected" From a182be9c78401b71b6675289f1e6c28e96a9c9d0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 19:42:48 +0000 Subject: [PATCH 070/448] feat(api): update via SDK Studio --- .stats.yml | 4 ++-- src/kernel/resources/browsers.py | 10 ++++++++++ src/kernel/types/browser_create_params.py | 6 ++++++ tests/api_resources/test_browsers.py | 2 ++ 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index a9ddb6d5..d6546666 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 11 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-b91d95f8e40f28d0e455d749b86c4d864ac15a264dcc2c5b317f626ff605ce2c.yml -openapi_spec_hash: befc3a683593ad7d832cfa9f0db941aa +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-4502c65bef0843a6ae96d23bba075433af6bab49b55b544b1522f63e7881c00c.yml +openapi_spec_hash: 3e67b77bbc8cd6155b8f66f3271f2643 config_hash: c6bab7ac8da570a5abbcfb19db119b6b diff --git a/src/kernel/resources/browsers.py b/src/kernel/resources/browsers.py index 375723ab..e3dc8336 100644 --- a/src/kernel/resources/browsers.py +++ b/src/kernel/resources/browsers.py @@ -49,6 +49,7 @@ def create( *, invocation_id: str, persistence: BrowserPersistenceParam | NotGiven = NOT_GIVEN, + stealth: 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, @@ -64,6 +65,9 @@ def create( persistence: Optional persistence configuration for the browser session. + stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot + mechanisms. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -78,6 +82,7 @@ def create( { "invocation_id": invocation_id, "persistence": persistence, + "stealth": stealth, }, browser_create_params.BrowserCreateParams, ), @@ -237,6 +242,7 @@ async def create( *, invocation_id: str, persistence: BrowserPersistenceParam | NotGiven = NOT_GIVEN, + stealth: 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, @@ -252,6 +258,9 @@ async def create( persistence: Optional persistence configuration for the browser session. + stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot + mechanisms. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -266,6 +275,7 @@ async def create( { "invocation_id": invocation_id, "persistence": persistence, + "stealth": stealth, }, browser_create_params.BrowserCreateParams, ), diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index 14fd5fe3..e50aefb2 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -15,3 +15,9 @@ class BrowserCreateParams(TypedDict, total=False): persistence: BrowserPersistenceParam """Optional persistence configuration for the browser session.""" + + stealth: bool + """ + If true, launches the browser in stealth mode to reduce detection by anti-bot + mechanisms. + """ diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index 260d1719..4593d2fb 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -35,6 +35,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: browser = client.browsers.create( invocation_id="rr33xuugxj9h0bkf1rdt2bet", persistence={"id": "my-awesome-browser-for-user-1234"}, + stealth=True, ) assert_matches_type(BrowserCreateResponse, browser, path=["response"]) @@ -228,6 +229,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> browser = await async_client.browsers.create( invocation_id="rr33xuugxj9h0bkf1rdt2bet", persistence={"id": "my-awesome-browser-for-user-1234"}, + stealth=True, ) assert_matches_type(BrowserCreateResponse, browser, path=["response"]) From fc6f753d4183daaa9802803455d1a7f0d7ac6d00 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 5 Jun 2025 14:39:33 +0000 Subject: [PATCH 071/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index da59f99e..2aca35ae 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.4.0" + ".": "0.5.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index ed0ec2d4..4c9fc235 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.4.0" +version = "0.5.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index e2017455..2c947c24 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.4.0" # x-release-please-version +__version__ = "0.5.0" # x-release-please-version From ed96e76a881e4173075f4227c1f41df9648ff3c8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 13 Jun 2025 02:10:30 +0000 Subject: [PATCH 072/448] chore(tests): run tests in parallel --- pyproject.toml | 3 ++- requirements-dev.lock | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4c9fc235..ce8b48d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ dev-dependencies = [ "importlib-metadata>=6.7.0", "rich>=13.7.1", "nest_asyncio==1.6.0", + "pytest-xdist>=3.6.1", ] [tool.rye.scripts] @@ -125,7 +126,7 @@ replacement = '[\1](https://github.com/onkernel/kernel-python-sdk/tree/main/\g<2 [tool.pytest.ini_options] testpaths = ["tests"] -addopts = "--tb=short" +addopts = "--tb=short -n auto" xfail_strict = true asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "session" diff --git a/requirements-dev.lock b/requirements-dev.lock index efd90ead..f40d9851 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -30,6 +30,8 @@ distro==1.8.0 exceptiongroup==1.2.2 # via anyio # via pytest +execnet==2.1.1 + # via pytest-xdist filelock==3.12.4 # via virtualenv h11==0.14.0 @@ -72,7 +74,9 @@ pygments==2.18.0 pyright==1.1.399 pytest==8.3.3 # via pytest-asyncio + # via pytest-xdist pytest-asyncio==0.24.0 +pytest-xdist==3.7.0 python-dateutil==2.8.2 # via time-machine pytz==2023.3.post1 From 61b562f8b06560c3cd2668c5591455a060cb2ca4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 13 Jun 2025 02:35:04 +0000 Subject: [PATCH 073/448] fix(client): correctly parse binary response | stream --- src/kernel/_base_client.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/kernel/_base_client.py b/src/kernel/_base_client.py index 785adea6..c86e919f 100644 --- a/src/kernel/_base_client.py +++ b/src/kernel/_base_client.py @@ -1071,7 +1071,14 @@ def _process_response( ) -> ResponseT: origin = get_origin(cast_to) or cast_to - if inspect.isclass(origin) and issubclass(origin, BaseAPIResponse): + if ( + inspect.isclass(origin) + and issubclass(origin, BaseAPIResponse) + # we only want to actually return the custom BaseAPIResponse class if we're + # returning the raw response, or if we're not streaming SSE, as if we're streaming + # SSE then `cast_to` doesn't actively reflect the type we need to parse into + and (not stream or bool(response.request.headers.get(RAW_RESPONSE_HEADER))) + ): if not issubclass(origin, APIResponse): raise TypeError(f"API Response types must subclass {APIResponse}; Received {origin}") @@ -1574,7 +1581,14 @@ async def _process_response( ) -> ResponseT: origin = get_origin(cast_to) or cast_to - if inspect.isclass(origin) and issubclass(origin, BaseAPIResponse): + if ( + inspect.isclass(origin) + and issubclass(origin, BaseAPIResponse) + # we only want to actually return the custom BaseAPIResponse class if we're + # returning the raw response, or if we're not streaming SSE, as if we're streaming + # SSE then `cast_to` doesn't actively reflect the type we need to parse into + and (not stream or bool(response.request.headers.get(RAW_RESPONSE_HEADER))) + ): if not issubclass(origin, AsyncAPIResponse): raise TypeError(f"API Response types must subclass {AsyncAPIResponse}; Received {origin}") From 3de9f3054297729bf903cfb19570c32aaead40c4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Jun 2025 19:42:26 +0000 Subject: [PATCH 074/448] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index d6546666..d2422bd1 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 11 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-4502c65bef0843a6ae96d23bba075433af6bab49b55b544b1522f63e7881c00c.yml -openapi_spec_hash: 3e67b77bbc8cd6155b8f66f3271f2643 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-fa302aa17477431aaa82682fe71bdbb519270815fdc917477e1d7e606411be50.yml +openapi_spec_hash: 291cb0245ba582712900f0fb5cf44ee4 config_hash: c6bab7ac8da570a5abbcfb19db119b6b From f0b2032608a2e7ec2830462fb1ba4dda5a7cbfe5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Jun 2025 19:47:29 +0000 Subject: [PATCH 075/448] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index d2422bd1..8826a54f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 11 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-fa302aa17477431aaa82682fe71bdbb519270815fdc917477e1d7e606411be50.yml -openapi_spec_hash: 291cb0245ba582712900f0fb5cf44ee4 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-e622f6886b1153050eb4ee9fda37fff8b36b38b52e5d247ea172deb2594bf9d6.yml +openapi_spec_hash: 3fa294f57c68b34e526a52bdd86eb562 config_hash: c6bab7ac8da570a5abbcfb19db119b6b From 26e565c9adac1dc3b84aef5d1e2856d023738509 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Jun 2025 19:59:19 +0000 Subject: [PATCH 076/448] feat(api): update via SDK Studio --- .stats.yml | 8 +- api.md | 18 + src/kernel/_client.py | 10 +- src/kernel/resources/__init__.py | 14 + src/kernel/resources/deployments.py | 407 ++++++++++++++++++ src/kernel/types/__init__.py | 4 + src/kernel/types/deployment_create_params.py | 33 ++ .../types/deployment_create_response.py | 35 ++ .../types/deployment_follow_response.py | 129 ++++++ .../types/deployment_retrieve_response.py | 35 ++ tests/api_resources/test_deployments.py | 304 +++++++++++++ 11 files changed, 992 insertions(+), 5 deletions(-) create mode 100644 src/kernel/resources/deployments.py create mode 100644 src/kernel/types/deployment_create_params.py create mode 100644 src/kernel/types/deployment_create_response.py create mode 100644 src/kernel/types/deployment_follow_response.py create mode 100644 src/kernel/types/deployment_retrieve_response.py create mode 100644 tests/api_resources/test_deployments.py diff --git a/.stats.yml b/.stats.yml index 8826a54f..f34bfc3d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 11 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-e622f6886b1153050eb4ee9fda37fff8b36b38b52e5d247ea172deb2594bf9d6.yml -openapi_spec_hash: 3fa294f57c68b34e526a52bdd86eb562 -config_hash: c6bab7ac8da570a5abbcfb19db119b6b +configured_endpoints: 14 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-d2dfee8d576aa73f6075e6da61228571cb2e844b969a06067e34e43eb7898554.yml +openapi_spec_hash: 9981744bf9c27426cdf721f7b27cf093 +config_hash: a085d1b39ddf0b26ee798501a9f47e20 diff --git a/api.md b/api.md index cbacba5d..db76da32 100644 --- a/api.md +++ b/api.md @@ -1,3 +1,21 @@ +# Deployments + +Types: + +```python +from kernel.types import ( + DeploymentCreateResponse, + DeploymentRetrieveResponse, + DeploymentFollowResponse, +) +``` + +Methods: + +- client.deployments.create(\*\*params) -> DeploymentCreateResponse +- client.deployments.retrieve(id) -> DeploymentRetrieveResponse +- client.deployments.follow(id) -> DeploymentFollowResponse + # Apps Types: diff --git a/src/kernel/_client.py b/src/kernel/_client.py index bf6fbb4d..084d2a59 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -21,7 +21,7 @@ ) from ._utils import is_given, get_async_library from ._version import __version__ -from .resources import browsers +from .resources import browsers, deployments from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import KernelError, APIStatusError from ._base_client import ( @@ -50,6 +50,7 @@ class Kernel(SyncAPIClient): + deployments: deployments.DeploymentsResource apps: apps.AppsResource browsers: browsers.BrowsersResource with_raw_response: KernelWithRawResponse @@ -133,6 +134,7 @@ def __init__( _strict_response_validation=_strict_response_validation, ) + self.deployments = deployments.DeploymentsResource(self) self.apps = apps.AppsResource(self) self.browsers = browsers.BrowsersResource(self) self.with_raw_response = KernelWithRawResponse(self) @@ -246,6 +248,7 @@ def _make_status_error( class AsyncKernel(AsyncAPIClient): + deployments: deployments.AsyncDeploymentsResource apps: apps.AsyncAppsResource browsers: browsers.AsyncBrowsersResource with_raw_response: AsyncKernelWithRawResponse @@ -329,6 +332,7 @@ def __init__( _strict_response_validation=_strict_response_validation, ) + self.deployments = deployments.AsyncDeploymentsResource(self) self.apps = apps.AsyncAppsResource(self) self.browsers = browsers.AsyncBrowsersResource(self) self.with_raw_response = AsyncKernelWithRawResponse(self) @@ -443,24 +447,28 @@ def _make_status_error( class KernelWithRawResponse: def __init__(self, client: Kernel) -> None: + self.deployments = deployments.DeploymentsResourceWithRawResponse(client.deployments) self.apps = apps.AppsResourceWithRawResponse(client.apps) self.browsers = browsers.BrowsersResourceWithRawResponse(client.browsers) class AsyncKernelWithRawResponse: def __init__(self, client: AsyncKernel) -> None: + self.deployments = deployments.AsyncDeploymentsResourceWithRawResponse(client.deployments) self.apps = apps.AsyncAppsResourceWithRawResponse(client.apps) self.browsers = browsers.AsyncBrowsersResourceWithRawResponse(client.browsers) class KernelWithStreamedResponse: def __init__(self, client: Kernel) -> None: + self.deployments = deployments.DeploymentsResourceWithStreamingResponse(client.deployments) self.apps = apps.AppsResourceWithStreamingResponse(client.apps) self.browsers = browsers.BrowsersResourceWithStreamingResponse(client.browsers) class AsyncKernelWithStreamedResponse: def __init__(self, client: AsyncKernel) -> None: + self.deployments = deployments.AsyncDeploymentsResourceWithStreamingResponse(client.deployments) self.apps = apps.AsyncAppsResourceWithStreamingResponse(client.apps) self.browsers = browsers.AsyncBrowsersResourceWithStreamingResponse(client.browsers) diff --git a/src/kernel/resources/__init__.py b/src/kernel/resources/__init__.py index 647bde62..f65d1db4 100644 --- a/src/kernel/resources/__init__.py +++ b/src/kernel/resources/__init__.py @@ -16,8 +16,22 @@ BrowsersResourceWithStreamingResponse, AsyncBrowsersResourceWithStreamingResponse, ) +from .deployments import ( + DeploymentsResource, + AsyncDeploymentsResource, + DeploymentsResourceWithRawResponse, + AsyncDeploymentsResourceWithRawResponse, + DeploymentsResourceWithStreamingResponse, + AsyncDeploymentsResourceWithStreamingResponse, +) __all__ = [ + "DeploymentsResource", + "AsyncDeploymentsResource", + "DeploymentsResourceWithRawResponse", + "AsyncDeploymentsResourceWithRawResponse", + "DeploymentsResourceWithStreamingResponse", + "AsyncDeploymentsResourceWithStreamingResponse", "AppsResource", "AsyncAppsResource", "AppsResourceWithRawResponse", diff --git a/src/kernel/resources/deployments.py b/src/kernel/resources/deployments.py new file mode 100644 index 00000000..6442ff0e --- /dev/null +++ b/src/kernel/resources/deployments.py @@ -0,0 +1,407 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Any, Dict, Mapping, cast +from typing_extensions import Literal + +import httpx + +from ..types import deployment_create_params +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes +from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._streaming import Stream, AsyncStream +from .._base_client import make_request_options +from ..types.deployment_create_response import DeploymentCreateResponse +from ..types.deployment_follow_response import DeploymentFollowResponse +from ..types.deployment_retrieve_response import DeploymentRetrieveResponse + +__all__ = ["DeploymentsResource", "AsyncDeploymentsResource"] + + +class DeploymentsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> DeploymentsResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return DeploymentsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> DeploymentsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return DeploymentsResourceWithStreamingResponse(self) + + def create( + self, + *, + entrypoint_rel_path: str, + file: FileTypes, + env_vars: Dict[str, str] | NotGiven = NOT_GIVEN, + force: bool | NotGiven = NOT_GIVEN, + region: Literal["aws.us-east-1a"] | NotGiven = NOT_GIVEN, + version: 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, + ) -> DeploymentCreateResponse: + """ + Create a new deployment. + + Args: + entrypoint_rel_path: Relative path to the entrypoint of the application + + file: ZIP file containing the application source directory + + env_vars: Map of environment variables to set for the deployed application. Each key-value + pair represents an environment variable. + + force: Allow overwriting an existing app version + + region: Region for deployment. Currently we only support "aws.us-east-1a" + + version: Version of the application. Can be any string. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "entrypoint_rel_path": entrypoint_rel_path, + "file": file, + "env_vars": env_vars, + "force": force, + "region": region, + "version": version, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return self._post( + "/deployments", + body=maybe_transform(body, deployment_create_params.DeploymentCreateParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=DeploymentCreateResponse, + ) + + def retrieve( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DeploymentRetrieveResponse: + """ + Get information about a deployment's status. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/deployments/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=DeploymentRetrieveResponse, + ) + + def follow( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Stream[DeploymentFollowResponse]: + """ + Establishes a Server-Sent Events (SSE) stream that delivers real-time logs and + status updates for a deployment. The stream terminates automatically once the + deployment reaches a terminal state. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} + return self._get( + f"/deployments/{id}/events", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=cast( + Any, DeploymentFollowResponse + ), # Union types cannot be passed in as arguments in the type system + stream=True, + stream_cls=Stream[DeploymentFollowResponse], + ) + + +class AsyncDeploymentsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncDeploymentsResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncDeploymentsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncDeploymentsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return AsyncDeploymentsResourceWithStreamingResponse(self) + + async def create( + self, + *, + entrypoint_rel_path: str, + file: FileTypes, + env_vars: Dict[str, str] | NotGiven = NOT_GIVEN, + force: bool | NotGiven = NOT_GIVEN, + region: Literal["aws.us-east-1a"] | NotGiven = NOT_GIVEN, + version: 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, + ) -> DeploymentCreateResponse: + """ + Create a new deployment. + + Args: + entrypoint_rel_path: Relative path to the entrypoint of the application + + file: ZIP file containing the application source directory + + env_vars: Map of environment variables to set for the deployed application. Each key-value + pair represents an environment variable. + + force: Allow overwriting an existing app version + + region: Region for deployment. Currently we only support "aws.us-east-1a" + + version: Version of the application. Can be any string. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "entrypoint_rel_path": entrypoint_rel_path, + "file": file, + "env_vars": env_vars, + "force": force, + "region": region, + "version": version, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return await self._post( + "/deployments", + body=await async_maybe_transform(body, deployment_create_params.DeploymentCreateParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=DeploymentCreateResponse, + ) + + async def retrieve( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DeploymentRetrieveResponse: + """ + Get information about a deployment's status. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/deployments/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=DeploymentRetrieveResponse, + ) + + async def follow( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncStream[DeploymentFollowResponse]: + """ + Establishes a Server-Sent Events (SSE) stream that delivers real-time logs and + status updates for a deployment. The stream terminates automatically once the + deployment reaches a terminal state. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} + return await self._get( + f"/deployments/{id}/events", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=cast( + Any, DeploymentFollowResponse + ), # Union types cannot be passed in as arguments in the type system + stream=True, + stream_cls=AsyncStream[DeploymentFollowResponse], + ) + + +class DeploymentsResourceWithRawResponse: + def __init__(self, deployments: DeploymentsResource) -> None: + self._deployments = deployments + + self.create = to_raw_response_wrapper( + deployments.create, + ) + self.retrieve = to_raw_response_wrapper( + deployments.retrieve, + ) + self.follow = to_raw_response_wrapper( + deployments.follow, + ) + + +class AsyncDeploymentsResourceWithRawResponse: + def __init__(self, deployments: AsyncDeploymentsResource) -> None: + self._deployments = deployments + + self.create = async_to_raw_response_wrapper( + deployments.create, + ) + self.retrieve = async_to_raw_response_wrapper( + deployments.retrieve, + ) + self.follow = async_to_raw_response_wrapper( + deployments.follow, + ) + + +class DeploymentsResourceWithStreamingResponse: + def __init__(self, deployments: DeploymentsResource) -> None: + self._deployments = deployments + + self.create = to_streamed_response_wrapper( + deployments.create, + ) + self.retrieve = to_streamed_response_wrapper( + deployments.retrieve, + ) + self.follow = to_streamed_response_wrapper( + deployments.follow, + ) + + +class AsyncDeploymentsResourceWithStreamingResponse: + def __init__(self, deployments: AsyncDeploymentsResource) -> None: + self._deployments = deployments + + self.create = async_to_streamed_response_wrapper( + deployments.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + deployments.retrieve, + ) + self.follow = async_to_streamed_response_wrapper( + deployments.follow, + ) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index d6ca9554..93a1ee8a 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -9,5 +9,9 @@ from .browser_delete_params import BrowserDeleteParams as BrowserDeleteParams from .browser_list_response import BrowserListResponse as BrowserListResponse from .browser_create_response import BrowserCreateResponse as BrowserCreateResponse +from .deployment_create_params import DeploymentCreateParams as DeploymentCreateParams from .browser_persistence_param import BrowserPersistenceParam as BrowserPersistenceParam from .browser_retrieve_response import BrowserRetrieveResponse as BrowserRetrieveResponse +from .deployment_create_response import DeploymentCreateResponse as DeploymentCreateResponse +from .deployment_follow_response import DeploymentFollowResponse as DeploymentFollowResponse +from .deployment_retrieve_response import DeploymentRetrieveResponse as DeploymentRetrieveResponse diff --git a/src/kernel/types/deployment_create_params.py b/src/kernel/types/deployment_create_params.py new file mode 100644 index 00000000..6701c0a8 --- /dev/null +++ b/src/kernel/types/deployment_create_params.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict +from typing_extensions import Literal, Required, TypedDict + +from .._types import FileTypes + +__all__ = ["DeploymentCreateParams"] + + +class DeploymentCreateParams(TypedDict, total=False): + entrypoint_rel_path: Required[str] + """Relative path to the entrypoint of the application""" + + file: Required[FileTypes] + """ZIP file containing the application source directory""" + + env_vars: Dict[str, str] + """Map of environment variables to set for the deployed application. + + Each key-value pair represents an environment variable. + """ + + force: bool + """Allow overwriting an existing app version""" + + region: Literal["aws.us-east-1a"] + """Region for deployment. Currently we only support "aws.us-east-1a" """ + + version: str + """Version of the application. Can be any string.""" diff --git a/src/kernel/types/deployment_create_response.py b/src/kernel/types/deployment_create_response.py new file mode 100644 index 00000000..0f5d2b29 --- /dev/null +++ b/src/kernel/types/deployment_create_response.py @@ -0,0 +1,35 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, Optional +from datetime import datetime +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["DeploymentCreateResponse"] + + +class DeploymentCreateResponse(BaseModel): + id: str + """Unique identifier for the deployment""" + + created_at: datetime + """Timestamp when the deployment was created""" + + region: str + """Deployment region code""" + + status: Literal["queued", "in_progress", "running", "failed", "stopped"] + """Current status of the deployment""" + + entrypoint_rel_path: Optional[str] = None + """Relative path to the application entrypoint""" + + env_vars: Optional[Dict[str, str]] = None + """Environment variables configured for this deployment""" + + status_reason: Optional[str] = None + """Status reason""" + + updated_at: Optional[datetime] = None + """Timestamp when the deployment was last updated""" diff --git a/src/kernel/types/deployment_follow_response.py b/src/kernel/types/deployment_follow_response.py new file mode 100644 index 00000000..09f1abcf --- /dev/null +++ b/src/kernel/types/deployment_follow_response.py @@ -0,0 +1,129 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, List, Union, Optional +from datetime import datetime +from typing_extensions import Literal, Annotated, TypeAlias + +from .._utils import PropertyInfo +from .._models import BaseModel + +__all__ = [ + "DeploymentFollowResponse", + "LogEvent", + "DeploymentStateEvent", + "DeploymentStateEventDeployment", + "AppVersionSummaryEvent", + "ErrorEvent", + "ErrorEventError", + "ErrorEventErrorDetail", + "ErrorEventErrorInnerError", +] + + +class LogEvent(BaseModel): + event: Literal["log"] + """Event type identifier (always "log").""" + + message: str + """Log message text.""" + + timestamp: Optional[datetime] = None + """Time the log entry was produced.""" + + +class DeploymentStateEventDeployment(BaseModel): + id: str + """Unique identifier for the deployment""" + + created_at: datetime + """Timestamp when the deployment was created""" + + region: str + """Deployment region code""" + + status: Literal["queued", "in_progress", "running", "failed", "stopped"] + """Current status of the deployment""" + + entrypoint_rel_path: Optional[str] = None + """Relative path to the application entrypoint""" + + env_vars: Optional[Dict[str, str]] = None + """Environment variables configured for this deployment""" + + status_reason: Optional[str] = None + """Status reason""" + + updated_at: Optional[datetime] = None + """Timestamp when the deployment was last updated""" + + +class DeploymentStateEvent(BaseModel): + deployment: DeploymentStateEventDeployment + """Deployment record information.""" + + event: Literal["deployment_state"] + """Event type identifier (always "deployment_state").""" + + timestamp: Optional[datetime] = None + """Time the state was reported.""" + + +class AppVersionSummaryEvent(BaseModel): + id: Optional[str] = None + """Unique identifier for the app version""" + + app_name: Optional[str] = None + """Name of the application""" + + env_vars: Optional[Dict[str, str]] = None + """Environment variables configured for this app version""" + + event: Optional[Literal["app_version_summary"]] = None + """Event type identifier (always "app_version_summary").""" + + region: Optional[str] = None + """Deployment region code""" + + version: Optional[str] = None + """Version label for the application""" + + +class ErrorEventErrorDetail(BaseModel): + code: Optional[str] = None + """Lower-level error code providing more specific detail""" + + message: Optional[str] = None + """Further detail about the error""" + + +class ErrorEventErrorInnerError(BaseModel): + code: Optional[str] = None + """Lower-level error code providing more specific detail""" + + message: Optional[str] = None + """Further detail about the error""" + + +class ErrorEventError(BaseModel): + code: str + """Application-specific error code (machine-readable)""" + + message: str + """Human-readable error description for debugging""" + + details: Optional[List[ErrorEventErrorDetail]] = None + """Additional error details (for multiple errors)""" + + inner_error: Optional[ErrorEventErrorInnerError] = None + + +class ErrorEvent(BaseModel): + error: Optional[ErrorEventError] = None + + event: Optional[Literal["error"]] = None + """Event type identifier (always "error").""" + + +DeploymentFollowResponse: TypeAlias = Annotated[ + Union[LogEvent, DeploymentStateEvent, AppVersionSummaryEvent, ErrorEvent], PropertyInfo(discriminator="event") +] diff --git a/src/kernel/types/deployment_retrieve_response.py b/src/kernel/types/deployment_retrieve_response.py new file mode 100644 index 00000000..efe9f7b0 --- /dev/null +++ b/src/kernel/types/deployment_retrieve_response.py @@ -0,0 +1,35 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, Optional +from datetime import datetime +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["DeploymentRetrieveResponse"] + + +class DeploymentRetrieveResponse(BaseModel): + id: str + """Unique identifier for the deployment""" + + created_at: datetime + """Timestamp when the deployment was created""" + + region: str + """Deployment region code""" + + status: Literal["queued", "in_progress", "running", "failed", "stopped"] + """Current status of the deployment""" + + entrypoint_rel_path: Optional[str] = None + """Relative path to the application entrypoint""" + + env_vars: Optional[Dict[str, str]] = None + """Environment variables configured for this deployment""" + + status_reason: Optional[str] = None + """Status reason""" + + updated_at: Optional[datetime] = None + """Timestamp when the deployment was last updated""" diff --git a/tests/api_resources/test_deployments.py b/tests/api_resources/test_deployments.py new file mode 100644 index 00000000..4bd80fca --- /dev/null +++ b/tests/api_resources/test_deployments.py @@ -0,0 +1,304 @@ +# 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 kernel import Kernel, AsyncKernel +from tests.utils import assert_matches_type +from kernel.types import DeploymentCreateResponse, DeploymentRetrieveResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestDeployments: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip() + @parametrize + def test_method_create(self, client: Kernel) -> None: + deployment = client.deployments.create( + entrypoint_rel_path="src/app.py", + file=b"raw file contents", + ) + assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_method_create_with_all_params(self, client: Kernel) -> None: + deployment = client.deployments.create( + entrypoint_rel_path="src/app.py", + file=b"raw file contents", + env_vars={"foo": "string"}, + force=False, + region="aws.us-east-1a", + version="1.0.0", + ) + assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_raw_response_create(self, client: Kernel) -> None: + response = client.deployments.with_raw_response.create( + entrypoint_rel_path="src/app.py", + file=b"raw file contents", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + deployment = response.parse() + assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_streaming_response_create(self, client: Kernel) -> None: + with client.deployments.with_streaming_response.create( + entrypoint_rel_path="src/app.py", + file=b"raw file contents", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + deployment = response.parse() + assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + def test_method_retrieve(self, client: Kernel) -> None: + deployment = client.deployments.retrieve( + "id", + ) + assert_matches_type(DeploymentRetrieveResponse, deployment, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_raw_response_retrieve(self, client: Kernel) -> None: + response = client.deployments.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + deployment = response.parse() + assert_matches_type(DeploymentRetrieveResponse, deployment, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_streaming_response_retrieve(self, client: Kernel) -> None: + with client.deployments.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + deployment = response.parse() + assert_matches_type(DeploymentRetrieveResponse, deployment, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + def test_path_params_retrieve(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.deployments.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + def test_method_follow(self, client: Kernel) -> None: + deployment_stream = client.deployments.follow( + "id", + ) + deployment_stream.response.close() + + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + def test_raw_response_follow(self, client: Kernel) -> None: + response = client.deployments.with_raw_response.follow( + "id", + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = response.parse() + stream.close() + + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + def test_streaming_response_follow(self, client: Kernel) -> None: + with client.deployments.with_streaming_response.follow( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = response.parse() + stream.close() + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + def test_path_params_follow(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.deployments.with_raw_response.follow( + "", + ) + + +class TestAsyncDeployments: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip() + @parametrize + async def test_method_create(self, async_client: AsyncKernel) -> None: + deployment = await async_client.deployments.create( + entrypoint_rel_path="src/app.py", + file=b"raw file contents", + ) + assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: + deployment = await async_client.deployments.create( + entrypoint_rel_path="src/app.py", + file=b"raw file contents", + env_vars={"foo": "string"}, + force=False, + region="aws.us-east-1a", + version="1.0.0", + ) + assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_raw_response_create(self, async_client: AsyncKernel) -> None: + response = await async_client.deployments.with_raw_response.create( + entrypoint_rel_path="src/app.py", + file=b"raw file contents", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + deployment = await response.parse() + assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: + async with async_client.deployments.with_streaming_response.create( + entrypoint_rel_path="src/app.py", + file=b"raw file contents", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + deployment = await response.parse() + assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + async def test_method_retrieve(self, async_client: AsyncKernel) -> None: + deployment = await async_client.deployments.retrieve( + "id", + ) + assert_matches_type(DeploymentRetrieveResponse, deployment, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: + response = await async_client.deployments.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + deployment = await response.parse() + assert_matches_type(DeploymentRetrieveResponse, deployment, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: + async with async_client.deployments.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + deployment = await response.parse() + assert_matches_type(DeploymentRetrieveResponse, deployment, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.deployments.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + async def test_method_follow(self, async_client: AsyncKernel) -> None: + deployment_stream = await async_client.deployments.follow( + "id", + ) + await deployment_stream.response.aclose() + + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + async def test_raw_response_follow(self, async_client: AsyncKernel) -> None: + response = await async_client.deployments.with_raw_response.follow( + "id", + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = await response.parse() + await stream.close() + + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + async def test_streaming_response_follow(self, async_client: AsyncKernel) -> None: + async with async_client.deployments.with_streaming_response.follow( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = await response.parse() + await stream.close() + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + async def test_path_params_follow(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.deployments.with_raw_response.follow( + "", + ) From 03b46018a8d548eaf7a96a85f41a043ebbdf982c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Jun 2025 20:04:00 +0000 Subject: [PATCH 077/448] feat(api): update via SDK Studio --- .stats.yml | 4 +-- .../types/apps/deployment_follow_response.py | 2 +- .../types/deployment_follow_response.py | 30 +++++++++++-------- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/.stats.yml b/.stats.yml index f34bfc3d..f219d4b6 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 14 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-d2dfee8d576aa73f6075e6da61228571cb2e844b969a06067e34e43eb7898554.yml -openapi_spec_hash: 9981744bf9c27426cdf721f7b27cf093 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-aec3b879aa30638614c6217afbafcf737f37ac78ef3a51186dbf7b6fbf9e91ef.yml +openapi_spec_hash: 0aba27c707612e35b4068b1d748dc379 config_hash: a085d1b39ddf0b26ee798501a9f47e20 diff --git a/src/kernel/types/apps/deployment_follow_response.py b/src/kernel/types/apps/deployment_follow_response.py index eb1ded77..cee006c6 100644 --- a/src/kernel/types/apps/deployment_follow_response.py +++ b/src/kernel/types/apps/deployment_follow_response.py @@ -41,7 +41,7 @@ class LogEvent(BaseModel): message: str """Log message text.""" - timestamp: Optional[datetime] = None + timestamp: datetime """Time the log entry was produced.""" diff --git a/src/kernel/types/deployment_follow_response.py b/src/kernel/types/deployment_follow_response.py index 09f1abcf..59830fa9 100644 --- a/src/kernel/types/deployment_follow_response.py +++ b/src/kernel/types/deployment_follow_response.py @@ -27,7 +27,7 @@ class LogEvent(BaseModel): message: str """Log message text.""" - timestamp: Optional[datetime] = None + timestamp: datetime """Time the log entry was produced.""" @@ -64,29 +64,32 @@ class DeploymentStateEvent(BaseModel): event: Literal["deployment_state"] """Event type identifier (always "deployment_state").""" - timestamp: Optional[datetime] = None + timestamp: datetime """Time the state was reported.""" class AppVersionSummaryEvent(BaseModel): - id: Optional[str] = None + id: str """Unique identifier for the app version""" - app_name: Optional[str] = None + app_name: str """Name of the application""" - env_vars: Optional[Dict[str, str]] = None - """Environment variables configured for this app version""" - - event: Optional[Literal["app_version_summary"]] = None + event: Literal["app_version_summary"] """Event type identifier (always "app_version_summary").""" - region: Optional[str] = None + region: str """Deployment region code""" - version: Optional[str] = None + timestamp: datetime + """Time the state was reported.""" + + version: str """Version label for the application""" + env_vars: Optional[Dict[str, str]] = None + """Environment variables configured for this app version""" + class ErrorEventErrorDetail(BaseModel): code: Optional[str] = None @@ -118,11 +121,14 @@ class ErrorEventError(BaseModel): class ErrorEvent(BaseModel): - error: Optional[ErrorEventError] = None + error: ErrorEventError - event: Optional[Literal["error"]] = None + event: Literal["error"] """Event type identifier (always "error").""" + timestamp: datetime + """Time the error occurred.""" + DeploymentFollowResponse: TypeAlias = Annotated[ Union[LogEvent, DeploymentStateEvent, AppVersionSummaryEvent, ErrorEvent], PropertyInfo(discriminator="event") From ae12dc05cec663002546538f28defcfa5336950b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Jun 2025 21:04:27 +0000 Subject: [PATCH 078/448] feat(api): update via SDK Studio --- .stats.yml | 4 ++-- src/kernel/types/deployment_follow_response.py | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index f219d4b6..c68e4154 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 14 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-aec3b879aa30638614c6217afbafcf737f37ac78ef3a51186dbf7b6fbf9e91ef.yml -openapi_spec_hash: 0aba27c707612e35b4068b1d748dc379 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-aac74422364f9d25e30fcefd510297580b77be4b84c71416c5b9de5b882e5945.yml +openapi_spec_hash: 4d42a5d93bd82754acf11e32e7438a04 config_hash: a085d1b39ddf0b26ee798501a9f47e20 diff --git a/src/kernel/types/deployment_follow_response.py b/src/kernel/types/deployment_follow_response.py index 59830fa9..60860c13 100644 --- a/src/kernel/types/deployment_follow_response.py +++ b/src/kernel/types/deployment_follow_response.py @@ -87,6 +87,9 @@ class AppVersionSummaryEvent(BaseModel): version: str """Version label for the application""" + actions: Optional[List[str]] = None + """List of actions available on the app""" + env_vars: Optional[Dict[str, str]] = None """Environment variables configured for this app version""" From 40eac49c75dc5dad225988b59dbf62550e67248e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Jun 2025 22:37:33 +0000 Subject: [PATCH 079/448] feat(api): update via SDK Studio --- .stats.yml | 4 ++-- src/kernel/types/app_list_response.py | 4 ++-- src/kernel/types/deployment_create_response.py | 2 +- src/kernel/types/deployment_follow_response.py | 4 ++-- src/kernel/types/deployment_retrieve_response.py | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.stats.yml b/.stats.yml index c68e4154..3f66d229 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 14 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-aac74422364f9d25e30fcefd510297580b77be4b84c71416c5b9de5b882e5945.yml -openapi_spec_hash: 4d42a5d93bd82754acf11e32e7438a04 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2fed6c2aef6fb20a2815d0ed36d801c566a73ea11a66db5d892b1533a1fac19e.yml +openapi_spec_hash: 55559a2ca985ed36cb8a13b09f80dcb5 config_hash: a085d1b39ddf0b26ee798501a9f47e20 diff --git a/src/kernel/types/app_list_response.py b/src/kernel/types/app_list_response.py index 8a6f6218..1d35fd2e 100644 --- a/src/kernel/types/app_list_response.py +++ b/src/kernel/types/app_list_response.py @@ -1,7 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import Dict, List, Optional -from typing_extensions import TypeAlias +from typing_extensions import Literal, TypeAlias from .._models import BaseModel @@ -15,7 +15,7 @@ class AppListResponseItem(BaseModel): app_name: str """Name of the application""" - region: str + region: Literal["aws.us-east-1a"] """Deployment region code""" version: str diff --git a/src/kernel/types/deployment_create_response.py b/src/kernel/types/deployment_create_response.py index 0f5d2b29..c14bf273 100644 --- a/src/kernel/types/deployment_create_response.py +++ b/src/kernel/types/deployment_create_response.py @@ -16,7 +16,7 @@ class DeploymentCreateResponse(BaseModel): created_at: datetime """Timestamp when the deployment was created""" - region: str + region: Literal["aws.us-east-1a"] """Deployment region code""" status: Literal["queued", "in_progress", "running", "failed", "stopped"] diff --git a/src/kernel/types/deployment_follow_response.py b/src/kernel/types/deployment_follow_response.py index 60860c13..bcc98a02 100644 --- a/src/kernel/types/deployment_follow_response.py +++ b/src/kernel/types/deployment_follow_response.py @@ -38,7 +38,7 @@ class DeploymentStateEventDeployment(BaseModel): created_at: datetime """Timestamp when the deployment was created""" - region: str + region: Literal["aws.us-east-1a"] """Deployment region code""" status: Literal["queued", "in_progress", "running", "failed", "stopped"] @@ -78,7 +78,7 @@ class AppVersionSummaryEvent(BaseModel): event: Literal["app_version_summary"] """Event type identifier (always "app_version_summary").""" - region: str + region: Literal["aws.us-east-1a"] """Deployment region code""" timestamp: datetime diff --git a/src/kernel/types/deployment_retrieve_response.py b/src/kernel/types/deployment_retrieve_response.py index efe9f7b0..28c0d4b9 100644 --- a/src/kernel/types/deployment_retrieve_response.py +++ b/src/kernel/types/deployment_retrieve_response.py @@ -16,7 +16,7 @@ class DeploymentRetrieveResponse(BaseModel): created_at: datetime """Timestamp when the deployment was created""" - region: str + region: Literal["aws.us-east-1a"] """Deployment region code""" status: Literal["queued", "in_progress", "running", "failed", "stopped"] From bb25ed492ee6dadd3a7336e083f299110d944f91 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Jun 2025 23:01:10 +0000 Subject: [PATCH 080/448] feat(api): update via SDK Studio --- .stats.yml | 4 ++-- src/kernel/types/deployment_follow_response.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.stats.yml b/.stats.yml index 3f66d229..4dea91fa 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 14 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2fed6c2aef6fb20a2815d0ed36d801c566a73ea11a66db5d892b1533a1fac19e.yml -openapi_spec_hash: 55559a2ca985ed36cb8a13b09f80dcb5 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-da3b6999bce525461011a620a559d34d4b4ab1d073758e7add4d2ba09f57a2ba.yml +openapi_spec_hash: 7bec5f31fa27666a3955076653c6ac40 config_hash: a085d1b39ddf0b26ee798501a9f47e20 diff --git a/src/kernel/types/deployment_follow_response.py b/src/kernel/types/deployment_follow_response.py index bcc98a02..757b51a8 100644 --- a/src/kernel/types/deployment_follow_response.py +++ b/src/kernel/types/deployment_follow_response.py @@ -72,6 +72,9 @@ class AppVersionSummaryEvent(BaseModel): id: str """Unique identifier for the app version""" + actions: List[str] + """List of actions available on the app""" + app_name: str """Name of the application""" @@ -87,9 +90,6 @@ class AppVersionSummaryEvent(BaseModel): version: str """Version label for the application""" - actions: Optional[List[str]] = None - """List of actions available on the app""" - env_vars: Optional[Dict[str, str]] = None """Environment variables configured for this app version""" From aabea0e22b4bd83d88b6e8c3c4cb28cae4acd8ca Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Jun 2025 23:12:42 +0000 Subject: [PATCH 081/448] feat(api): update via SDK Studio --- .stats.yml | 4 ++-- src/kernel/types/deployment_follow_response.py | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 4dea91fa..3ea11abf 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 14 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-da3b6999bce525461011a620a559d34d4b4ab1d073758e7add4d2ba09f57a2ba.yml -openapi_spec_hash: 7bec5f31fa27666a3955076653c6ac40 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-b8c3224543bfd828075063a87302ec205b54f8b24658cc869b98aa81d995d855.yml +openapi_spec_hash: 52f5b821303fef54e61bae285f185200 config_hash: a085d1b39ddf0b26ee798501a9f47e20 diff --git a/src/kernel/types/deployment_follow_response.py b/src/kernel/types/deployment_follow_response.py index 757b51a8..44a17a64 100644 --- a/src/kernel/types/deployment_follow_response.py +++ b/src/kernel/types/deployment_follow_response.py @@ -13,6 +13,7 @@ "DeploymentStateEvent", "DeploymentStateEventDeployment", "AppVersionSummaryEvent", + "AppVersionSummaryEventAction", "ErrorEvent", "ErrorEventError", "ErrorEventErrorDetail", @@ -68,11 +69,16 @@ class DeploymentStateEvent(BaseModel): """Time the state was reported.""" +class AppVersionSummaryEventAction(BaseModel): + name: str + """Name of the action""" + + class AppVersionSummaryEvent(BaseModel): id: str """Unique identifier for the app version""" - actions: List[str] + actions: List[AppVersionSummaryEventAction] """List of actions available on the app""" app_name: str From d5078db36dc9efa8afd2b8c82237614ae8535a1e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Jun 2025 23:13:55 +0000 Subject: [PATCH 082/448] feat(api): update via SDK Studio --- .stats.yml | 4 ++-- src/kernel/types/deployment_follow_response.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 3ea11abf..3493617f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 14 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-b8c3224543bfd828075063a87302ec205b54f8b24658cc869b98aa81d995d855.yml -openapi_spec_hash: 52f5b821303fef54e61bae285f185200 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-ba02d679c34c3af5ea47ec2b1a7387785d831e09f35bebfef9f05538ff380c3b.yml +openapi_spec_hash: 7ddbbe7354f65437d4eb567e8b042552 config_hash: a085d1b39ddf0b26ee798501a9f47e20 diff --git a/src/kernel/types/deployment_follow_response.py b/src/kernel/types/deployment_follow_response.py index 44a17a64..58203f83 100644 --- a/src/kernel/types/deployment_follow_response.py +++ b/src/kernel/types/deployment_follow_response.py @@ -70,7 +70,7 @@ class DeploymentStateEvent(BaseModel): class AppVersionSummaryEventAction(BaseModel): - name: str + name: Optional[str] = None """Name of the action""" From cfc1b8450ebdf20b9cdd84dcff9b223d97dbe404 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Jun 2025 23:14:42 +0000 Subject: [PATCH 083/448] feat(api): update via SDK Studio --- .stats.yml | 4 ++-- src/kernel/types/deployment_follow_response.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 3493617f..bb234450 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 14 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-ba02d679c34c3af5ea47ec2b1a7387785d831e09f35bebfef9f05538ff380c3b.yml -openapi_spec_hash: 7ddbbe7354f65437d4eb567e8b042552 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-f7fa782f119b02d610bac1dbc75bf8355e73169d978997527f643e24036dabdd.yml +openapi_spec_hash: 9543dfe156b1c42a2fe4d3767e6b0778 config_hash: a085d1b39ddf0b26ee798501a9f47e20 diff --git a/src/kernel/types/deployment_follow_response.py b/src/kernel/types/deployment_follow_response.py index 58203f83..44a17a64 100644 --- a/src/kernel/types/deployment_follow_response.py +++ b/src/kernel/types/deployment_follow_response.py @@ -70,7 +70,7 @@ class DeploymentStateEvent(BaseModel): class AppVersionSummaryEventAction(BaseModel): - name: Optional[str] = None + name: str """Name of the action""" From f0b2a9d69b9d493d738ef45481824395937357a2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 02:39:24 +0000 Subject: [PATCH 084/448] chore(tests): add tests for httpx client instantiation & proxies --- tests/test_client.py | 53 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/tests/test_client.py b/tests/test_client.py index 575e4a4c..25c8f4ac 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -27,7 +27,14 @@ from kernel._models import BaseModel, FinalRequestOptions from kernel._constants import RAW_RESPONSE_HEADER from kernel._exceptions import KernelError, APIStatusError, APITimeoutError, APIResponseValidationError -from kernel._base_client import DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, BaseClient, make_request_options +from kernel._base_client import ( + DEFAULT_TIMEOUT, + HTTPX_DEFAULT_TIMEOUT, + BaseClient, + DefaultHttpxClient, + DefaultAsyncHttpxClient, + make_request_options, +) from kernel.types.browser_create_params import BrowserCreateParams from .utils import update_env @@ -835,6 +842,28 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: assert response.http_request.headers.get("x-stainless-retry-count") == "42" + def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: + # Test that the proxy environment variables are set correctly + monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + + client = DefaultHttpxClient() + + mounts = tuple(client._mounts.items()) + assert len(mounts) == 1 + assert mounts[0][0].pattern == "https://" + + @pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning") + def test_default_client_creation(self) -> None: + # Ensure that the client can be initialized without any exceptions + DefaultHttpxClient( + verify=True, + cert=None, + trust_env=True, + http1=True, + http2=False, + limits=httpx.Limits(max_connections=100, max_keepalive_connections=20), + ) + @pytest.mark.respx(base_url=base_url) def test_follow_redirects(self, respx_mock: MockRouter) -> None: # Test that the default follow_redirects=True allows following redirects @@ -1712,6 +1741,28 @@ async def test_main() -> None: time.sleep(0.1) + async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: + # Test that the proxy environment variables are set correctly + monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + + client = DefaultAsyncHttpxClient() + + mounts = tuple(client._mounts.items()) + assert len(mounts) == 1 + assert mounts[0][0].pattern == "https://" + + @pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning") + async def test_default_client_creation(self) -> None: + # Ensure that the client can be initialized without any exceptions + DefaultAsyncHttpxClient( + verify=True, + cert=None, + trust_env=True, + http1=True, + http2=False, + limits=httpx.Limits(max_connections=100, max_keepalive_connections=20), + ) + @pytest.mark.respx(base_url=base_url) async def test_follow_redirects(self, respx_mock: MockRouter) -> None: # Test that the default follow_redirects=True allows following redirects From 88373134a28e86d1b9cd99a5647e78f3513322b3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 04:09:23 +0000 Subject: [PATCH 085/448] chore(internal): update conftest.py --- tests/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 6d3cc202..3a11d3f1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,5 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + from __future__ import annotations import os From ee9c845a378400b9e22a4bb9f81d4fb3de447cd7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 06:39:59 +0000 Subject: [PATCH 086/448] chore(ci): enable for pull requests --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51b16df4..c3f5bc46 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,10 @@ on: - 'integrated/**' - 'stl-preview-head/**' - 'stl-preview-base/**' + pull_request: + branches-ignore: + - 'stl-preview-head/**' + - 'stl-preview-base/**' jobs: lint: From 3604203aa2aaaca09299e5bba60e9cdb0e513d98 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 15:09:43 +0000 Subject: [PATCH 087/448] feat(api): update via SDK Studio --- .stats.yml | 8 +- api.md | 20 ++- src/kernel/_client.py | 10 +- src/kernel/resources/__init__.py | 14 ++ src/kernel/resources/apps/__init__.py | 14 -- src/kernel/resources/apps/apps.py | 32 ---- .../resources/{apps => }/invocations.py | 115 +++++++++++-- src/kernel/types/__init__.py | 9 ++ src/kernel/types/apps/__init__.py | 5 - .../types/apps/deployment_follow_response.py | 14 +- .../types/deployment_follow_response.py | 76 +-------- src/kernel/types/deployment_state_event.py | 46 ++++++ .../{apps => }/invocation_create_params.py | 2 +- .../{apps => }/invocation_create_response.py | 2 +- .../types/invocation_follow_response.py | 41 +++++ .../invocation_retrieve_response.py | 2 +- src/kernel/types/invocation_state_event.py | 57 +++++++ .../{apps => }/invocation_update_params.py | 0 .../{apps => }/invocation_update_response.py | 2 +- src/kernel/types/shared/__init__.py | 4 + src/kernel/types/shared/error_detail.py | 15 ++ src/kernel/types/shared/log_event.py | 19 +++ .../{apps => }/test_invocations.py | 152 ++++++++++++++---- 23 files changed, 474 insertions(+), 185 deletions(-) rename src/kernel/resources/{apps => }/invocations.py (75%) create mode 100644 src/kernel/types/deployment_state_event.py rename src/kernel/types/{apps => }/invocation_create_params.py (95%) rename src/kernel/types/{apps => }/invocation_create_response.py (95%) create mode 100644 src/kernel/types/invocation_follow_response.py rename src/kernel/types/{apps => }/invocation_retrieve_response.py (97%) create mode 100644 src/kernel/types/invocation_state_event.py rename src/kernel/types/{apps => }/invocation_update_params.py (100%) rename src/kernel/types/{apps => }/invocation_update_response.py (97%) create mode 100644 src/kernel/types/shared/__init__.py create mode 100644 src/kernel/types/shared/error_detail.py create mode 100644 src/kernel/types/shared/log_event.py rename tests/api_resources/{apps => }/test_invocations.py (65%) diff --git a/.stats.yml b/.stats.yml index bb234450..b912099a 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 14 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-f7fa782f119b02d610bac1dbc75bf8355e73169d978997527f643e24036dabdd.yml -openapi_spec_hash: 9543dfe156b1c42a2fe4d3767e6b0778 -config_hash: a085d1b39ddf0b26ee798501a9f47e20 +configured_endpoints: 15 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-5d4e11bc46eeecee7363d56a9dfe946acee997d5b352c2b0a50c20e742c54d2d.yml +openapi_spec_hash: 333e53ad9c706296b9afdb8ff73bec8f +config_hash: 4e2f9aebc2153d5caf7bb8b2eb107026 diff --git a/api.md b/api.md index db76da32..9a7d9a7d 100644 --- a/api.md +++ b/api.md @@ -1,9 +1,16 @@ +# Shared Types + +```python +from kernel.types import ErrorDetail, LogEvent +``` + # Deployments Types: ```python from kernel.types import ( + DeploymentStateEvent, DeploymentCreateResponse, DeploymentRetrieveResponse, DeploymentFollowResponse, @@ -41,23 +48,26 @@ Methods: - client.apps.deployments.create(\*\*params) -> DeploymentCreateResponse - client.apps.deployments.follow(id) -> DeploymentFollowResponse -## Invocations +# Invocations Types: ```python -from kernel.types.apps import ( +from kernel.types import ( + InvocationStateEvent, InvocationCreateResponse, InvocationRetrieveResponse, InvocationUpdateResponse, + InvocationFollowResponse, ) ``` Methods: -- client.apps.invocations.create(\*\*params) -> InvocationCreateResponse -- client.apps.invocations.retrieve(id) -> InvocationRetrieveResponse -- client.apps.invocations.update(id, \*\*params) -> InvocationUpdateResponse +- client.invocations.create(\*\*params) -> InvocationCreateResponse +- client.invocations.retrieve(id) -> InvocationRetrieveResponse +- client.invocations.update(id, \*\*params) -> InvocationUpdateResponse +- client.invocations.follow(id) -> InvocationFollowResponse # Browsers diff --git a/src/kernel/_client.py b/src/kernel/_client.py index 084d2a59..63a7dc92 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -21,7 +21,7 @@ ) from ._utils import is_given, get_async_library from ._version import __version__ -from .resources import browsers, deployments +from .resources import browsers, deployments, invocations from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import KernelError, APIStatusError from ._base_client import ( @@ -52,6 +52,7 @@ class Kernel(SyncAPIClient): deployments: deployments.DeploymentsResource apps: apps.AppsResource + invocations: invocations.InvocationsResource browsers: browsers.BrowsersResource with_raw_response: KernelWithRawResponse with_streaming_response: KernelWithStreamedResponse @@ -136,6 +137,7 @@ def __init__( self.deployments = deployments.DeploymentsResource(self) self.apps = apps.AppsResource(self) + self.invocations = invocations.InvocationsResource(self) self.browsers = browsers.BrowsersResource(self) self.with_raw_response = KernelWithRawResponse(self) self.with_streaming_response = KernelWithStreamedResponse(self) @@ -250,6 +252,7 @@ def _make_status_error( class AsyncKernel(AsyncAPIClient): deployments: deployments.AsyncDeploymentsResource apps: apps.AsyncAppsResource + invocations: invocations.AsyncInvocationsResource browsers: browsers.AsyncBrowsersResource with_raw_response: AsyncKernelWithRawResponse with_streaming_response: AsyncKernelWithStreamedResponse @@ -334,6 +337,7 @@ def __init__( self.deployments = deployments.AsyncDeploymentsResource(self) self.apps = apps.AsyncAppsResource(self) + self.invocations = invocations.AsyncInvocationsResource(self) self.browsers = browsers.AsyncBrowsersResource(self) self.with_raw_response = AsyncKernelWithRawResponse(self) self.with_streaming_response = AsyncKernelWithStreamedResponse(self) @@ -449,6 +453,7 @@ class KernelWithRawResponse: def __init__(self, client: Kernel) -> None: self.deployments = deployments.DeploymentsResourceWithRawResponse(client.deployments) self.apps = apps.AppsResourceWithRawResponse(client.apps) + self.invocations = invocations.InvocationsResourceWithRawResponse(client.invocations) self.browsers = browsers.BrowsersResourceWithRawResponse(client.browsers) @@ -456,6 +461,7 @@ class AsyncKernelWithRawResponse: def __init__(self, client: AsyncKernel) -> None: self.deployments = deployments.AsyncDeploymentsResourceWithRawResponse(client.deployments) self.apps = apps.AsyncAppsResourceWithRawResponse(client.apps) + self.invocations = invocations.AsyncInvocationsResourceWithRawResponse(client.invocations) self.browsers = browsers.AsyncBrowsersResourceWithRawResponse(client.browsers) @@ -463,6 +469,7 @@ class KernelWithStreamedResponse: def __init__(self, client: Kernel) -> None: self.deployments = deployments.DeploymentsResourceWithStreamingResponse(client.deployments) self.apps = apps.AppsResourceWithStreamingResponse(client.apps) + self.invocations = invocations.InvocationsResourceWithStreamingResponse(client.invocations) self.browsers = browsers.BrowsersResourceWithStreamingResponse(client.browsers) @@ -470,6 +477,7 @@ class AsyncKernelWithStreamedResponse: def __init__(self, client: AsyncKernel) -> None: self.deployments = deployments.AsyncDeploymentsResourceWithStreamingResponse(client.deployments) self.apps = apps.AsyncAppsResourceWithStreamingResponse(client.apps) + self.invocations = invocations.AsyncInvocationsResourceWithStreamingResponse(client.invocations) self.browsers = browsers.AsyncBrowsersResourceWithStreamingResponse(client.browsers) diff --git a/src/kernel/resources/__init__.py b/src/kernel/resources/__init__.py index f65d1db4..3b6a4d62 100644 --- a/src/kernel/resources/__init__.py +++ b/src/kernel/resources/__init__.py @@ -24,6 +24,14 @@ DeploymentsResourceWithStreamingResponse, AsyncDeploymentsResourceWithStreamingResponse, ) +from .invocations import ( + InvocationsResource, + AsyncInvocationsResource, + InvocationsResourceWithRawResponse, + AsyncInvocationsResourceWithRawResponse, + InvocationsResourceWithStreamingResponse, + AsyncInvocationsResourceWithStreamingResponse, +) __all__ = [ "DeploymentsResource", @@ -38,6 +46,12 @@ "AsyncAppsResourceWithRawResponse", "AppsResourceWithStreamingResponse", "AsyncAppsResourceWithStreamingResponse", + "InvocationsResource", + "AsyncInvocationsResource", + "InvocationsResourceWithRawResponse", + "AsyncInvocationsResourceWithRawResponse", + "InvocationsResourceWithStreamingResponse", + "AsyncInvocationsResourceWithStreamingResponse", "BrowsersResource", "AsyncBrowsersResource", "BrowsersResourceWithRawResponse", diff --git a/src/kernel/resources/apps/__init__.py b/src/kernel/resources/apps/__init__.py index 5602ad74..6ce731d2 100644 --- a/src/kernel/resources/apps/__init__.py +++ b/src/kernel/resources/apps/__init__.py @@ -16,14 +16,6 @@ DeploymentsResourceWithStreamingResponse, AsyncDeploymentsResourceWithStreamingResponse, ) -from .invocations import ( - InvocationsResource, - AsyncInvocationsResource, - InvocationsResourceWithRawResponse, - AsyncInvocationsResourceWithRawResponse, - InvocationsResourceWithStreamingResponse, - AsyncInvocationsResourceWithStreamingResponse, -) __all__ = [ "DeploymentsResource", @@ -32,12 +24,6 @@ "AsyncDeploymentsResourceWithRawResponse", "DeploymentsResourceWithStreamingResponse", "AsyncDeploymentsResourceWithStreamingResponse", - "InvocationsResource", - "AsyncInvocationsResource", - "InvocationsResourceWithRawResponse", - "AsyncInvocationsResourceWithRawResponse", - "InvocationsResourceWithStreamingResponse", - "AsyncInvocationsResourceWithStreamingResponse", "AppsResource", "AsyncAppsResource", "AppsResourceWithRawResponse", diff --git a/src/kernel/resources/apps/apps.py b/src/kernel/resources/apps/apps.py index 3769bd57..726db204 100644 --- a/src/kernel/resources/apps/apps.py +++ b/src/kernel/resources/apps/apps.py @@ -23,14 +23,6 @@ DeploymentsResourceWithStreamingResponse, AsyncDeploymentsResourceWithStreamingResponse, ) -from .invocations import ( - InvocationsResource, - AsyncInvocationsResource, - InvocationsResourceWithRawResponse, - AsyncInvocationsResourceWithRawResponse, - InvocationsResourceWithStreamingResponse, - AsyncInvocationsResourceWithStreamingResponse, -) from ..._base_client import make_request_options from ...types.app_list_response import AppListResponse @@ -42,10 +34,6 @@ class AppsResource(SyncAPIResource): def deployments(self) -> DeploymentsResource: return DeploymentsResource(self._client) - @cached_property - def invocations(self) -> InvocationsResource: - return InvocationsResource(self._client) - @cached_property def with_raw_response(self) -> AppsResourceWithRawResponse: """ @@ -118,10 +106,6 @@ class AsyncAppsResource(AsyncAPIResource): def deployments(self) -> AsyncDeploymentsResource: return AsyncDeploymentsResource(self._client) - @cached_property - def invocations(self) -> AsyncInvocationsResource: - return AsyncInvocationsResource(self._client) - @cached_property def with_raw_response(self) -> AsyncAppsResourceWithRawResponse: """ @@ -201,10 +185,6 @@ def __init__(self, apps: AppsResource) -> None: def deployments(self) -> DeploymentsResourceWithRawResponse: return DeploymentsResourceWithRawResponse(self._apps.deployments) - @cached_property - def invocations(self) -> InvocationsResourceWithRawResponse: - return InvocationsResourceWithRawResponse(self._apps.invocations) - class AsyncAppsResourceWithRawResponse: def __init__(self, apps: AsyncAppsResource) -> None: @@ -218,10 +198,6 @@ def __init__(self, apps: AsyncAppsResource) -> None: def deployments(self) -> AsyncDeploymentsResourceWithRawResponse: return AsyncDeploymentsResourceWithRawResponse(self._apps.deployments) - @cached_property - def invocations(self) -> AsyncInvocationsResourceWithRawResponse: - return AsyncInvocationsResourceWithRawResponse(self._apps.invocations) - class AppsResourceWithStreamingResponse: def __init__(self, apps: AppsResource) -> None: @@ -235,10 +211,6 @@ def __init__(self, apps: AppsResource) -> None: def deployments(self) -> DeploymentsResourceWithStreamingResponse: return DeploymentsResourceWithStreamingResponse(self._apps.deployments) - @cached_property - def invocations(self) -> InvocationsResourceWithStreamingResponse: - return InvocationsResourceWithStreamingResponse(self._apps.invocations) - class AsyncAppsResourceWithStreamingResponse: def __init__(self, apps: AsyncAppsResource) -> None: @@ -251,7 +223,3 @@ def __init__(self, apps: AsyncAppsResource) -> None: @cached_property def deployments(self) -> AsyncDeploymentsResourceWithStreamingResponse: return AsyncDeploymentsResourceWithStreamingResponse(self._apps.deployments) - - @cached_property - def invocations(self) -> AsyncInvocationsResourceWithStreamingResponse: - return AsyncInvocationsResourceWithStreamingResponse(self._apps.invocations) diff --git a/src/kernel/resources/apps/invocations.py b/src/kernel/resources/invocations.py similarity index 75% rename from src/kernel/resources/apps/invocations.py rename to src/kernel/resources/invocations.py index b5413d4f..c87b8d79 100644 --- a/src/kernel/resources/apps/invocations.py +++ b/src/kernel/resources/invocations.py @@ -2,25 +2,28 @@ from __future__ import annotations +from typing import Any, cast 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 ( +from ..types import invocation_create_params, invocation_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 ...types.apps import invocation_create_params, invocation_update_params -from ..._base_client import make_request_options -from ...types.apps.invocation_create_response import InvocationCreateResponse -from ...types.apps.invocation_update_response import InvocationUpdateResponse -from ...types.apps.invocation_retrieve_response import InvocationRetrieveResponse +from .._streaming import Stream, AsyncStream +from .._base_client import make_request_options +from ..types.invocation_create_response import InvocationCreateResponse +from ..types.invocation_follow_response import InvocationFollowResponse +from ..types.invocation_update_response import InvocationUpdateResponse +from ..types.invocation_retrieve_response import InvocationRetrieveResponse __all__ = ["InvocationsResource", "AsyncInvocationsResource"] @@ -180,6 +183,46 @@ def update( cast_to=InvocationUpdateResponse, ) + def follow( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Stream[InvocationFollowResponse]: + """ + Establishes a Server-Sent Events (SSE) stream that delivers real-time logs and + status updates for an invocation. The stream terminates automatically once the + invocation reaches a terminal state. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} + return self._get( + f"/invocations/{id}/events", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=cast( + Any, InvocationFollowResponse + ), # Union types cannot be passed in as arguments in the type system + stream=True, + stream_cls=Stream[InvocationFollowResponse], + ) + class AsyncInvocationsResource(AsyncAPIResource): @cached_property @@ -336,6 +379,46 @@ async def update( cast_to=InvocationUpdateResponse, ) + async def follow( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncStream[InvocationFollowResponse]: + """ + Establishes a Server-Sent Events (SSE) stream that delivers real-time logs and + status updates for an invocation. The stream terminates automatically once the + invocation reaches a terminal state. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} + return await self._get( + f"/invocations/{id}/events", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=cast( + Any, InvocationFollowResponse + ), # Union types cannot be passed in as arguments in the type system + stream=True, + stream_cls=AsyncStream[InvocationFollowResponse], + ) + class InvocationsResourceWithRawResponse: def __init__(self, invocations: InvocationsResource) -> None: @@ -350,6 +433,9 @@ def __init__(self, invocations: InvocationsResource) -> None: self.update = to_raw_response_wrapper( invocations.update, ) + self.follow = to_raw_response_wrapper( + invocations.follow, + ) class AsyncInvocationsResourceWithRawResponse: @@ -365,6 +451,9 @@ def __init__(self, invocations: AsyncInvocationsResource) -> None: self.update = async_to_raw_response_wrapper( invocations.update, ) + self.follow = async_to_raw_response_wrapper( + invocations.follow, + ) class InvocationsResourceWithStreamingResponse: @@ -380,6 +469,9 @@ def __init__(self, invocations: InvocationsResource) -> None: self.update = to_streamed_response_wrapper( invocations.update, ) + self.follow = to_streamed_response_wrapper( + invocations.follow, + ) class AsyncInvocationsResourceWithStreamingResponse: @@ -395,3 +487,6 @@ def __init__(self, invocations: AsyncInvocationsResource) -> None: self.update = async_to_streamed_response_wrapper( invocations.update, ) + self.follow = async_to_streamed_response_wrapper( + invocations.follow, + ) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 93a1ee8a..4a5c229e 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -2,16 +2,25 @@ from __future__ import annotations +from .shared import LogEvent as LogEvent, ErrorDetail as ErrorDetail from .app_list_params import AppListParams as AppListParams from .app_list_response import AppListResponse as AppListResponse from .browser_persistence import BrowserPersistence as BrowserPersistence from .browser_create_params import BrowserCreateParams as BrowserCreateParams from .browser_delete_params import BrowserDeleteParams as BrowserDeleteParams from .browser_list_response import BrowserListResponse as BrowserListResponse +from .deployment_state_event import DeploymentStateEvent as DeploymentStateEvent +from .invocation_state_event import InvocationStateEvent as InvocationStateEvent from .browser_create_response import BrowserCreateResponse as BrowserCreateResponse from .deployment_create_params import DeploymentCreateParams as DeploymentCreateParams +from .invocation_create_params import InvocationCreateParams as InvocationCreateParams +from .invocation_update_params import InvocationUpdateParams as InvocationUpdateParams from .browser_persistence_param import BrowserPersistenceParam as BrowserPersistenceParam from .browser_retrieve_response import BrowserRetrieveResponse as BrowserRetrieveResponse from .deployment_create_response import DeploymentCreateResponse as DeploymentCreateResponse from .deployment_follow_response import DeploymentFollowResponse as DeploymentFollowResponse +from .invocation_create_response import InvocationCreateResponse as InvocationCreateResponse +from .invocation_follow_response import InvocationFollowResponse as InvocationFollowResponse +from .invocation_update_response import InvocationUpdateResponse as InvocationUpdateResponse from .deployment_retrieve_response import DeploymentRetrieveResponse as DeploymentRetrieveResponse +from .invocation_retrieve_response import InvocationRetrieveResponse as InvocationRetrieveResponse diff --git a/src/kernel/types/apps/__init__.py b/src/kernel/types/apps/__init__.py index f4bf7a25..93aed9dd 100644 --- a/src/kernel/types/apps/__init__.py +++ b/src/kernel/types/apps/__init__.py @@ -3,10 +3,5 @@ from __future__ import annotations from .deployment_create_params import DeploymentCreateParams as DeploymentCreateParams -from .invocation_create_params import InvocationCreateParams as InvocationCreateParams -from .invocation_update_params import InvocationUpdateParams as InvocationUpdateParams from .deployment_create_response import DeploymentCreateResponse as DeploymentCreateResponse from .deployment_follow_response import DeploymentFollowResponse as DeploymentFollowResponse -from .invocation_create_response import InvocationCreateResponse as InvocationCreateResponse -from .invocation_update_response import InvocationUpdateResponse as InvocationUpdateResponse -from .invocation_retrieve_response import InvocationRetrieveResponse as InvocationRetrieveResponse diff --git a/src/kernel/types/apps/deployment_follow_response.py b/src/kernel/types/apps/deployment_follow_response.py index cee006c6..fae1b2b4 100644 --- a/src/kernel/types/apps/deployment_follow_response.py +++ b/src/kernel/types/apps/deployment_follow_response.py @@ -6,8 +6,9 @@ from ..._utils import PropertyInfo from ..._models import BaseModel +from ..shared.log_event import LogEvent -__all__ = ["DeploymentFollowResponse", "StateEvent", "StateUpdateEvent", "LogEvent"] +__all__ = ["DeploymentFollowResponse", "StateEvent", "StateUpdateEvent"] class StateEvent(BaseModel): @@ -34,17 +35,6 @@ class StateUpdateEvent(BaseModel): """Time the state change occurred.""" -class LogEvent(BaseModel): - event: Literal["log"] - """Event type identifier (always "log").""" - - message: str - """Log message text.""" - - timestamp: datetime - """Time the log entry was produced.""" - - DeploymentFollowResponse: TypeAlias = Annotated[ Union[StateEvent, StateUpdateEvent, LogEvent], PropertyInfo(discriminator="event") ] diff --git a/src/kernel/types/deployment_follow_response.py b/src/kernel/types/deployment_follow_response.py index 44a17a64..38afcadb 100644 --- a/src/kernel/types/deployment_follow_response.py +++ b/src/kernel/types/deployment_follow_response.py @@ -6,69 +6,19 @@ from .._utils import PropertyInfo from .._models import BaseModel +from .shared.log_event import LogEvent +from .shared.error_detail import ErrorDetail +from .deployment_state_event import DeploymentStateEvent __all__ = [ "DeploymentFollowResponse", - "LogEvent", - "DeploymentStateEvent", - "DeploymentStateEventDeployment", "AppVersionSummaryEvent", "AppVersionSummaryEventAction", "ErrorEvent", "ErrorEventError", - "ErrorEventErrorDetail", - "ErrorEventErrorInnerError", ] -class LogEvent(BaseModel): - event: Literal["log"] - """Event type identifier (always "log").""" - - message: str - """Log message text.""" - - timestamp: datetime - """Time the log entry was produced.""" - - -class DeploymentStateEventDeployment(BaseModel): - id: str - """Unique identifier for the deployment""" - - created_at: datetime - """Timestamp when the deployment was created""" - - region: Literal["aws.us-east-1a"] - """Deployment region code""" - - status: Literal["queued", "in_progress", "running", "failed", "stopped"] - """Current status of the deployment""" - - entrypoint_rel_path: Optional[str] = None - """Relative path to the application entrypoint""" - - env_vars: Optional[Dict[str, str]] = None - """Environment variables configured for this deployment""" - - status_reason: Optional[str] = None - """Status reason""" - - updated_at: Optional[datetime] = None - """Timestamp when the deployment was last updated""" - - -class DeploymentStateEvent(BaseModel): - deployment: DeploymentStateEventDeployment - """Deployment record information.""" - - event: Literal["deployment_state"] - """Event type identifier (always "deployment_state").""" - - timestamp: datetime - """Time the state was reported.""" - - class AppVersionSummaryEventAction(BaseModel): name: str """Name of the action""" @@ -100,22 +50,6 @@ class AppVersionSummaryEvent(BaseModel): """Environment variables configured for this app version""" -class ErrorEventErrorDetail(BaseModel): - code: Optional[str] = None - """Lower-level error code providing more specific detail""" - - message: Optional[str] = None - """Further detail about the error""" - - -class ErrorEventErrorInnerError(BaseModel): - code: Optional[str] = None - """Lower-level error code providing more specific detail""" - - message: Optional[str] = None - """Further detail about the error""" - - class ErrorEventError(BaseModel): code: str """Application-specific error code (machine-readable)""" @@ -123,10 +57,10 @@ class ErrorEventError(BaseModel): message: str """Human-readable error description for debugging""" - details: Optional[List[ErrorEventErrorDetail]] = None + details: Optional[List[ErrorDetail]] = None """Additional error details (for multiple errors)""" - inner_error: Optional[ErrorEventErrorInnerError] = None + inner_error: Optional[ErrorDetail] = None class ErrorEvent(BaseModel): diff --git a/src/kernel/types/deployment_state_event.py b/src/kernel/types/deployment_state_event.py new file mode 100644 index 00000000..572d51bc --- /dev/null +++ b/src/kernel/types/deployment_state_event.py @@ -0,0 +1,46 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, Optional +from datetime import datetime +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["DeploymentStateEvent", "Deployment"] + + +class Deployment(BaseModel): + id: str + """Unique identifier for the deployment""" + + created_at: datetime + """Timestamp when the deployment was created""" + + region: Literal["aws.us-east-1a"] + """Deployment region code""" + + status: Literal["queued", "in_progress", "running", "failed", "stopped"] + """Current status of the deployment""" + + entrypoint_rel_path: Optional[str] = None + """Relative path to the application entrypoint""" + + env_vars: Optional[Dict[str, str]] = None + """Environment variables configured for this deployment""" + + status_reason: Optional[str] = None + """Status reason""" + + updated_at: Optional[datetime] = None + """Timestamp when the deployment was last updated""" + + +class DeploymentStateEvent(BaseModel): + deployment: Deployment + """Deployment record information.""" + + event: Literal["deployment_state"] + """Event type identifier (always "deployment_state").""" + + timestamp: datetime + """Time the state was reported.""" diff --git a/src/kernel/types/apps/invocation_create_params.py b/src/kernel/types/invocation_create_params.py similarity index 95% rename from src/kernel/types/apps/invocation_create_params.py rename to src/kernel/types/invocation_create_params.py index 01035ae5..1d6bc64e 100644 --- a/src/kernel/types/apps/invocation_create_params.py +++ b/src/kernel/types/invocation_create_params.py @@ -4,7 +4,7 @@ from typing_extensions import Required, Annotated, TypedDict -from ..._utils import PropertyInfo +from .._utils import PropertyInfo __all__ = ["InvocationCreateParams"] diff --git a/src/kernel/types/apps/invocation_create_response.py b/src/kernel/types/invocation_create_response.py similarity index 95% rename from src/kernel/types/apps/invocation_create_response.py rename to src/kernel/types/invocation_create_response.py index df4a1664..d58f2623 100644 --- a/src/kernel/types/apps/invocation_create_response.py +++ b/src/kernel/types/invocation_create_response.py @@ -3,7 +3,7 @@ from typing import Optional from typing_extensions import Literal -from ..._models import BaseModel +from .._models import BaseModel __all__ = ["InvocationCreateResponse"] diff --git a/src/kernel/types/invocation_follow_response.py b/src/kernel/types/invocation_follow_response.py new file mode 100644 index 00000000..b1693a17 --- /dev/null +++ b/src/kernel/types/invocation_follow_response.py @@ -0,0 +1,41 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Union, Optional +from datetime import datetime +from typing_extensions import Literal, Annotated, TypeAlias + +from .._utils import PropertyInfo +from .._models import BaseModel +from .shared.log_event import LogEvent +from .shared.error_detail import ErrorDetail +from .invocation_state_event import InvocationStateEvent + +__all__ = ["InvocationFollowResponse", "ErrorEvent", "ErrorEventError"] + + +class ErrorEventError(BaseModel): + code: str + """Application-specific error code (machine-readable)""" + + message: str + """Human-readable error description for debugging""" + + details: Optional[List[ErrorDetail]] = None + """Additional error details (for multiple errors)""" + + inner_error: Optional[ErrorDetail] = None + + +class ErrorEvent(BaseModel): + error: ErrorEventError + + event: Literal["error"] + """Event type identifier (always "error").""" + + timestamp: datetime + """Time the error occurred.""" + + +InvocationFollowResponse: TypeAlias = Annotated[ + Union[LogEvent, InvocationStateEvent, ErrorEvent], PropertyInfo(discriminator="event") +] diff --git a/src/kernel/types/apps/invocation_retrieve_response.py b/src/kernel/types/invocation_retrieve_response.py similarity index 97% rename from src/kernel/types/apps/invocation_retrieve_response.py rename to src/kernel/types/invocation_retrieve_response.py index f328b146..6626b53e 100644 --- a/src/kernel/types/apps/invocation_retrieve_response.py +++ b/src/kernel/types/invocation_retrieve_response.py @@ -4,7 +4,7 @@ from datetime import datetime from typing_extensions import Literal -from ..._models import BaseModel +from .._models import BaseModel __all__ = ["InvocationRetrieveResponse"] diff --git a/src/kernel/types/invocation_state_event.py b/src/kernel/types/invocation_state_event.py new file mode 100644 index 00000000..6f30ea69 --- /dev/null +++ b/src/kernel/types/invocation_state_event.py @@ -0,0 +1,57 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["InvocationStateEvent", "Invocation"] + + +class Invocation(BaseModel): + id: str + """ID of the invocation""" + + action_name: str + """Name of the action invoked""" + + app_name: str + """Name of the application""" + + started_at: datetime + """RFC 3339 Nanoseconds timestamp when the invocation started""" + + status: Literal["queued", "running", "succeeded", "failed"] + """Status of the invocation""" + + finished_at: Optional[datetime] = None + """ + RFC 3339 Nanoseconds timestamp when the invocation finished (null if still + running) + """ + + output: Optional[str] = None + """Output produced by the action, rendered as a JSON string. + + This could be: string, number, boolean, array, object, or null. + """ + + payload: Optional[str] = None + """Payload provided to the invocation. + + This is a string that can be parsed as JSON. + """ + + status_reason: Optional[str] = None + """Status reason""" + + +class InvocationStateEvent(BaseModel): + event: Literal["invocation_state"] + """Event type identifier (always "invocation_state").""" + + invocation: Invocation + + timestamp: datetime + """Time the state was reported.""" diff --git a/src/kernel/types/apps/invocation_update_params.py b/src/kernel/types/invocation_update_params.py similarity index 100% rename from src/kernel/types/apps/invocation_update_params.py rename to src/kernel/types/invocation_update_params.py diff --git a/src/kernel/types/apps/invocation_update_response.py b/src/kernel/types/invocation_update_response.py similarity index 97% rename from src/kernel/types/apps/invocation_update_response.py rename to src/kernel/types/invocation_update_response.py index c30fc160..e0029a9c 100644 --- a/src/kernel/types/apps/invocation_update_response.py +++ b/src/kernel/types/invocation_update_response.py @@ -4,7 +4,7 @@ from datetime import datetime from typing_extensions import Literal -from ..._models import BaseModel +from .._models import BaseModel __all__ = ["InvocationUpdateResponse"] diff --git a/src/kernel/types/shared/__init__.py b/src/kernel/types/shared/__init__.py new file mode 100644 index 00000000..1f139b00 --- /dev/null +++ b/src/kernel/types/shared/__init__.py @@ -0,0 +1,4 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .log_event import LogEvent as LogEvent +from .error_detail import ErrorDetail as ErrorDetail diff --git a/src/kernel/types/shared/error_detail.py b/src/kernel/types/shared/error_detail.py new file mode 100644 index 00000000..24e655fc --- /dev/null +++ b/src/kernel/types/shared/error_detail.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = ["ErrorDetail"] + + +class ErrorDetail(BaseModel): + code: Optional[str] = None + """Lower-level error code providing more specific detail""" + + message: Optional[str] = None + """Further detail about the error""" diff --git a/src/kernel/types/shared/log_event.py b/src/kernel/types/shared/log_event.py new file mode 100644 index 00000000..69dbc561 --- /dev/null +++ b/src/kernel/types/shared/log_event.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from datetime import datetime +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["LogEvent"] + + +class LogEvent(BaseModel): + event: Literal["log"] + """Event type identifier (always "log").""" + + message: str + """Log message text.""" + + timestamp: datetime + """Time the log entry was produced.""" diff --git a/tests/api_resources/apps/test_invocations.py b/tests/api_resources/test_invocations.py similarity index 65% rename from tests/api_resources/apps/test_invocations.py rename to tests/api_resources/test_invocations.py index 87dc31fc..c11ea7c8 100644 --- a/tests/api_resources/apps/test_invocations.py +++ b/tests/api_resources/test_invocations.py @@ -9,7 +9,7 @@ from kernel import Kernel, AsyncKernel from tests.utils import assert_matches_type -from kernel.types.apps import ( +from kernel.types import ( InvocationCreateResponse, InvocationUpdateResponse, InvocationRetrieveResponse, @@ -24,7 +24,7 @@ class TestInvocations: @pytest.mark.skip() @parametrize def test_method_create(self, client: Kernel) -> None: - invocation = client.apps.invocations.create( + invocation = client.invocations.create( action_name="analyze", app_name="my-app", version="1.0.0", @@ -34,7 +34,7 @@ def test_method_create(self, client: Kernel) -> None: @pytest.mark.skip() @parametrize def test_method_create_with_all_params(self, client: Kernel) -> None: - invocation = client.apps.invocations.create( + invocation = client.invocations.create( action_name="analyze", app_name="my-app", version="1.0.0", @@ -46,7 +46,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: @pytest.mark.skip() @parametrize def test_raw_response_create(self, client: Kernel) -> None: - response = client.apps.invocations.with_raw_response.create( + response = client.invocations.with_raw_response.create( action_name="analyze", app_name="my-app", version="1.0.0", @@ -60,7 +60,7 @@ def test_raw_response_create(self, client: Kernel) -> None: @pytest.mark.skip() @parametrize def test_streaming_response_create(self, client: Kernel) -> None: - with client.apps.invocations.with_streaming_response.create( + with client.invocations.with_streaming_response.create( action_name="analyze", app_name="my-app", version="1.0.0", @@ -76,7 +76,7 @@ def test_streaming_response_create(self, client: Kernel) -> None: @pytest.mark.skip() @parametrize def test_method_retrieve(self, client: Kernel) -> None: - invocation = client.apps.invocations.retrieve( + invocation = client.invocations.retrieve( "id", ) assert_matches_type(InvocationRetrieveResponse, invocation, path=["response"]) @@ -84,7 +84,7 @@ def test_method_retrieve(self, client: Kernel) -> None: @pytest.mark.skip() @parametrize def test_raw_response_retrieve(self, client: Kernel) -> None: - response = client.apps.invocations.with_raw_response.retrieve( + response = client.invocations.with_raw_response.retrieve( "id", ) @@ -96,7 +96,7 @@ def test_raw_response_retrieve(self, client: Kernel) -> None: @pytest.mark.skip() @parametrize def test_streaming_response_retrieve(self, client: Kernel) -> None: - with client.apps.invocations.with_streaming_response.retrieve( + with client.invocations.with_streaming_response.retrieve( "id", ) as response: assert not response.is_closed @@ -111,14 +111,14 @@ def test_streaming_response_retrieve(self, client: Kernel) -> None: @parametrize def test_path_params_retrieve(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - client.apps.invocations.with_raw_response.retrieve( + client.invocations.with_raw_response.retrieve( "", ) @pytest.mark.skip() @parametrize def test_method_update(self, client: Kernel) -> None: - invocation = client.apps.invocations.update( + invocation = client.invocations.update( id="id", status="succeeded", ) @@ -127,7 +127,7 @@ def test_method_update(self, client: Kernel) -> None: @pytest.mark.skip() @parametrize def test_method_update_with_all_params(self, client: Kernel) -> None: - invocation = client.apps.invocations.update( + invocation = client.invocations.update( id="id", status="succeeded", output="output", @@ -137,7 +137,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None: @pytest.mark.skip() @parametrize def test_raw_response_update(self, client: Kernel) -> None: - response = client.apps.invocations.with_raw_response.update( + response = client.invocations.with_raw_response.update( id="id", status="succeeded", ) @@ -150,7 +150,7 @@ def test_raw_response_update(self, client: Kernel) -> None: @pytest.mark.skip() @parametrize def test_streaming_response_update(self, client: Kernel) -> None: - with client.apps.invocations.with_streaming_response.update( + with client.invocations.with_streaming_response.update( id="id", status="succeeded", ) as response: @@ -166,11 +166,60 @@ def test_streaming_response_update(self, client: Kernel) -> None: @parametrize def test_path_params_update(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - client.apps.invocations.with_raw_response.update( + client.invocations.with_raw_response.update( id="", status="succeeded", ) + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + def test_method_follow(self, client: Kernel) -> None: + invocation_stream = client.invocations.follow( + "id", + ) + invocation_stream.response.close() + + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + def test_raw_response_follow(self, client: Kernel) -> None: + response = client.invocations.with_raw_response.follow( + "id", + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = response.parse() + stream.close() + + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + def test_streaming_response_follow(self, client: Kernel) -> None: + with client.invocations.with_streaming_response.follow( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = response.parse() + stream.close() + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + def test_path_params_follow(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.invocations.with_raw_response.follow( + "", + ) + class TestAsyncInvocations: parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) @@ -178,7 +227,7 @@ class TestAsyncInvocations: @pytest.mark.skip() @parametrize async def test_method_create(self, async_client: AsyncKernel) -> None: - invocation = await async_client.apps.invocations.create( + invocation = await async_client.invocations.create( action_name="analyze", app_name="my-app", version="1.0.0", @@ -188,7 +237,7 @@ async def test_method_create(self, async_client: AsyncKernel) -> None: @pytest.mark.skip() @parametrize async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: - invocation = await async_client.apps.invocations.create( + invocation = await async_client.invocations.create( action_name="analyze", app_name="my-app", version="1.0.0", @@ -200,7 +249,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> @pytest.mark.skip() @parametrize async def test_raw_response_create(self, async_client: AsyncKernel) -> None: - response = await async_client.apps.invocations.with_raw_response.create( + response = await async_client.invocations.with_raw_response.create( action_name="analyze", app_name="my-app", version="1.0.0", @@ -214,7 +263,7 @@ async def test_raw_response_create(self, async_client: AsyncKernel) -> None: @pytest.mark.skip() @parametrize async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: - async with async_client.apps.invocations.with_streaming_response.create( + async with async_client.invocations.with_streaming_response.create( action_name="analyze", app_name="my-app", version="1.0.0", @@ -230,7 +279,7 @@ async def test_streaming_response_create(self, async_client: AsyncKernel) -> Non @pytest.mark.skip() @parametrize async def test_method_retrieve(self, async_client: AsyncKernel) -> None: - invocation = await async_client.apps.invocations.retrieve( + invocation = await async_client.invocations.retrieve( "id", ) assert_matches_type(InvocationRetrieveResponse, invocation, path=["response"]) @@ -238,7 +287,7 @@ async def test_method_retrieve(self, async_client: AsyncKernel) -> None: @pytest.mark.skip() @parametrize async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: - response = await async_client.apps.invocations.with_raw_response.retrieve( + response = await async_client.invocations.with_raw_response.retrieve( "id", ) @@ -250,7 +299,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: @pytest.mark.skip() @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: - async with async_client.apps.invocations.with_streaming_response.retrieve( + async with async_client.invocations.with_streaming_response.retrieve( "id", ) as response: assert not response.is_closed @@ -265,14 +314,14 @@ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> N @parametrize async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - await async_client.apps.invocations.with_raw_response.retrieve( + await async_client.invocations.with_raw_response.retrieve( "", ) @pytest.mark.skip() @parametrize async def test_method_update(self, async_client: AsyncKernel) -> None: - invocation = await async_client.apps.invocations.update( + invocation = await async_client.invocations.update( id="id", status="succeeded", ) @@ -281,7 +330,7 @@ async def test_method_update(self, async_client: AsyncKernel) -> None: @pytest.mark.skip() @parametrize async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> None: - invocation = await async_client.apps.invocations.update( + invocation = await async_client.invocations.update( id="id", status="succeeded", output="output", @@ -291,7 +340,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> @pytest.mark.skip() @parametrize async def test_raw_response_update(self, async_client: AsyncKernel) -> None: - response = await async_client.apps.invocations.with_raw_response.update( + response = await async_client.invocations.with_raw_response.update( id="id", status="succeeded", ) @@ -304,7 +353,7 @@ async def test_raw_response_update(self, async_client: AsyncKernel) -> None: @pytest.mark.skip() @parametrize async def test_streaming_response_update(self, async_client: AsyncKernel) -> None: - async with async_client.apps.invocations.with_streaming_response.update( + async with async_client.invocations.with_streaming_response.update( id="id", status="succeeded", ) as response: @@ -320,7 +369,56 @@ async def test_streaming_response_update(self, async_client: AsyncKernel) -> Non @parametrize async def test_path_params_update(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - await async_client.apps.invocations.with_raw_response.update( + await async_client.invocations.with_raw_response.update( id="", status="succeeded", ) + + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + async def test_method_follow(self, async_client: AsyncKernel) -> None: + invocation_stream = await async_client.invocations.follow( + "id", + ) + await invocation_stream.response.aclose() + + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + async def test_raw_response_follow(self, async_client: AsyncKernel) -> None: + response = await async_client.invocations.with_raw_response.follow( + "id", + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = await response.parse() + await stream.close() + + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + async def test_streaming_response_follow(self, async_client: AsyncKernel) -> None: + async with async_client.invocations.with_streaming_response.follow( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = await response.parse() + await stream.close() + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + async def test_path_params_follow(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.invocations.with_raw_response.follow( + "", + ) From 5ab81d7c2a2071cd8118493af79ad9bb2c319eb3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 15:20:37 +0000 Subject: [PATCH 088/448] feat(api): update via SDK Studio --- .stats.yml | 6 +-- api.md | 2 +- src/kernel/types/__init__.py | 2 +- .../types/deployment_follow_response.py | 33 +---------------- .../types/invocation_follow_response.py | 37 +++---------------- src/kernel/types/shared/__init__.py | 2 + src/kernel/types/shared/error.py | 21 +++++++++++ src/kernel/types/shared/error_event.py | 19 ++++++++++ 8 files changed, 55 insertions(+), 67 deletions(-) create mode 100644 src/kernel/types/shared/error.py create mode 100644 src/kernel/types/shared/error_event.py diff --git a/.stats.yml b/.stats.yml index b912099a..763dad39 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 15 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-5d4e11bc46eeecee7363d56a9dfe946acee997d5b352c2b0a50c20e742c54d2d.yml -openapi_spec_hash: 333e53ad9c706296b9afdb8ff73bec8f -config_hash: 4e2f9aebc2153d5caf7bb8b2eb107026 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-b1b412b00906fca75bfa73cff7267dbb5bf975581778072e0a90c73ad7ba9cb1.yml +openapi_spec_hash: 9b7a1b29bcb4963fe6da37005c357d27 +config_hash: df959c379e1145106030a4869b006afe diff --git a/api.md b/api.md index 9a7d9a7d..b3f657cc 100644 --- a/api.md +++ b/api.md @@ -1,7 +1,7 @@ # Shared Types ```python -from kernel.types import ErrorDetail, LogEvent +from kernel.types import Error, ErrorDetail, ErrorEvent, LogEvent ``` # Deployments diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 4a5c229e..ff41497c 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations -from .shared import LogEvent as LogEvent, ErrorDetail as ErrorDetail +from .shared import Error as Error, LogEvent as LogEvent, ErrorEvent as ErrorEvent, ErrorDetail as ErrorDetail from .app_list_params import AppListParams as AppListParams from .app_list_response import AppListResponse as AppListResponse from .browser_persistence import BrowserPersistence as BrowserPersistence diff --git a/src/kernel/types/deployment_follow_response.py b/src/kernel/types/deployment_follow_response.py index 38afcadb..e95ce262 100644 --- a/src/kernel/types/deployment_follow_response.py +++ b/src/kernel/types/deployment_follow_response.py @@ -7,16 +7,10 @@ from .._utils import PropertyInfo from .._models import BaseModel from .shared.log_event import LogEvent -from .shared.error_detail import ErrorDetail +from .shared.error_event import ErrorEvent from .deployment_state_event import DeploymentStateEvent -__all__ = [ - "DeploymentFollowResponse", - "AppVersionSummaryEvent", - "AppVersionSummaryEventAction", - "ErrorEvent", - "ErrorEventError", -] +__all__ = ["DeploymentFollowResponse", "AppVersionSummaryEvent", "AppVersionSummaryEventAction"] class AppVersionSummaryEventAction(BaseModel): @@ -50,29 +44,6 @@ class AppVersionSummaryEvent(BaseModel): """Environment variables configured for this app version""" -class ErrorEventError(BaseModel): - code: str - """Application-specific error code (machine-readable)""" - - message: str - """Human-readable error description for debugging""" - - details: Optional[List[ErrorDetail]] = None - """Additional error details (for multiple errors)""" - - inner_error: Optional[ErrorDetail] = None - - -class ErrorEvent(BaseModel): - error: ErrorEventError - - event: Literal["error"] - """Event type identifier (always "error").""" - - timestamp: datetime - """Time the error occurred.""" - - DeploymentFollowResponse: TypeAlias = Annotated[ Union[LogEvent, DeploymentStateEvent, AppVersionSummaryEvent, ErrorEvent], PropertyInfo(discriminator="event") ] diff --git a/src/kernel/types/invocation_follow_response.py b/src/kernel/types/invocation_follow_response.py index b1693a17..1abbafc7 100644 --- a/src/kernel/types/invocation_follow_response.py +++ b/src/kernel/types/invocation_follow_response.py @@ -1,41 +1,16 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List, Union, Optional -from datetime import datetime -from typing_extensions import Literal, Annotated, TypeAlias +from typing import Union +from typing_extensions import Annotated, TypeAlias from .._utils import PropertyInfo -from .._models import BaseModel from .shared.log_event import LogEvent -from .shared.error_detail import ErrorDetail +from .shared.error_event import ErrorEvent +from .deployment_state_event import DeploymentStateEvent from .invocation_state_event import InvocationStateEvent -__all__ = ["InvocationFollowResponse", "ErrorEvent", "ErrorEventError"] - - -class ErrorEventError(BaseModel): - code: str - """Application-specific error code (machine-readable)""" - - message: str - """Human-readable error description for debugging""" - - details: Optional[List[ErrorDetail]] = None - """Additional error details (for multiple errors)""" - - inner_error: Optional[ErrorDetail] = None - - -class ErrorEvent(BaseModel): - error: ErrorEventError - - event: Literal["error"] - """Event type identifier (always "error").""" - - timestamp: datetime - """Time the error occurred.""" - +__all__ = ["InvocationFollowResponse"] InvocationFollowResponse: TypeAlias = Annotated[ - Union[LogEvent, InvocationStateEvent, ErrorEvent], PropertyInfo(discriminator="event") + Union[LogEvent, DeploymentStateEvent, InvocationStateEvent, ErrorEvent], PropertyInfo(discriminator="event") ] diff --git a/src/kernel/types/shared/__init__.py b/src/kernel/types/shared/__init__.py index 1f139b00..f7c487b8 100644 --- a/src/kernel/types/shared/__init__.py +++ b/src/kernel/types/shared/__init__.py @@ -1,4 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from .error import Error as Error from .log_event import LogEvent as LogEvent +from .error_event import ErrorEvent as ErrorEvent from .error_detail import ErrorDetail as ErrorDetail diff --git a/src/kernel/types/shared/error.py b/src/kernel/types/shared/error.py new file mode 100644 index 00000000..47c8aae7 --- /dev/null +++ b/src/kernel/types/shared/error.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from ..._models import BaseModel +from .error_detail import ErrorDetail + +__all__ = ["Error"] + + +class Error(BaseModel): + code: str + """Application-specific error code (machine-readable)""" + + message: str + """Human-readable error description for debugging""" + + details: Optional[List[ErrorDetail]] = None + """Additional error details (for multiple errors)""" + + inner_error: Optional[ErrorDetail] = None diff --git a/src/kernel/types/shared/error_event.py b/src/kernel/types/shared/error_event.py new file mode 100644 index 00000000..542634b2 --- /dev/null +++ b/src/kernel/types/shared/error_event.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from datetime import datetime +from typing_extensions import Literal + +from .error import Error +from ..._models import BaseModel + +__all__ = ["ErrorEvent"] + + +class ErrorEvent(BaseModel): + error: Error + + event: Literal["error"] + """Event type identifier (always "error").""" + + timestamp: datetime + """Time the error occurred.""" From 83a44bd2f658a9ba52ce17a73ac690fcc970cf93 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 15:23:13 +0000 Subject: [PATCH 089/448] feat(api): update via SDK Studio --- .stats.yml | 4 ++-- src/kernel/types/invocation_follow_response.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.stats.yml b/.stats.yml index 763dad39..1e9f62b0 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 15 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-b1b412b00906fca75bfa73cff7267dbb5bf975581778072e0a90c73ad7ba9cb1.yml -openapi_spec_hash: 9b7a1b29bcb4963fe6da37005c357d27 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-5d4e11bc46eeecee7363d56a9dfe946acee997d5b352c2b0a50c20e742c54d2d.yml +openapi_spec_hash: 333e53ad9c706296b9afdb8ff73bec8f config_hash: df959c379e1145106030a4869b006afe diff --git a/src/kernel/types/invocation_follow_response.py b/src/kernel/types/invocation_follow_response.py index 1abbafc7..e3d7e8ef 100644 --- a/src/kernel/types/invocation_follow_response.py +++ b/src/kernel/types/invocation_follow_response.py @@ -6,11 +6,10 @@ from .._utils import PropertyInfo from .shared.log_event import LogEvent from .shared.error_event import ErrorEvent -from .deployment_state_event import DeploymentStateEvent from .invocation_state_event import InvocationStateEvent __all__ = ["InvocationFollowResponse"] InvocationFollowResponse: TypeAlias = Annotated[ - Union[LogEvent, DeploymentStateEvent, InvocationStateEvent, ErrorEvent], PropertyInfo(discriminator="event") + Union[LogEvent, InvocationStateEvent, ErrorEvent], PropertyInfo(discriminator="event") ] From ae0da3a186e1048d3c9aff7e82210fee52e7fb9a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 15:25:14 +0000 Subject: [PATCH 090/448] feat(api): update via SDK Studio --- .stats.yml | 2 +- api.md | 2 +- src/kernel/types/__init__.py | 2 +- src/kernel/types/shared/__init__.py | 1 - src/kernel/types/shared/error.py | 21 --------------------- src/kernel/types/shared/error_event.py | 18 ++++++++++++++++-- 6 files changed, 19 insertions(+), 27 deletions(-) delete mode 100644 src/kernel/types/shared/error.py diff --git a/.stats.yml b/.stats.yml index 1e9f62b0..71dae95c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 15 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-5d4e11bc46eeecee7363d56a9dfe946acee997d5b352c2b0a50c20e742c54d2d.yml openapi_spec_hash: 333e53ad9c706296b9afdb8ff73bec8f -config_hash: df959c379e1145106030a4869b006afe +config_hash: 79af9b3bec53ee798dddcf815befa25d diff --git a/api.md b/api.md index b3f657cc..4574ce71 100644 --- a/api.md +++ b/api.md @@ -1,7 +1,7 @@ # Shared Types ```python -from kernel.types import Error, ErrorDetail, ErrorEvent, LogEvent +from kernel.types import ErrorDetail, ErrorEvent, LogEvent ``` # Deployments diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index ff41497c..87e9ac8e 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations -from .shared import Error as Error, LogEvent as LogEvent, ErrorEvent as ErrorEvent, ErrorDetail as ErrorDetail +from .shared import LogEvent as LogEvent, ErrorEvent as ErrorEvent, ErrorDetail as ErrorDetail from .app_list_params import AppListParams as AppListParams from .app_list_response import AppListResponse as AppListResponse from .browser_persistence import BrowserPersistence as BrowserPersistence diff --git a/src/kernel/types/shared/__init__.py b/src/kernel/types/shared/__init__.py index f7c487b8..45c73b34 100644 --- a/src/kernel/types/shared/__init__.py +++ b/src/kernel/types/shared/__init__.py @@ -1,6 +1,5 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from .error import Error as Error from .log_event import LogEvent as LogEvent from .error_event import ErrorEvent as ErrorEvent from .error_detail import ErrorDetail as ErrorDetail diff --git a/src/kernel/types/shared/error.py b/src/kernel/types/shared/error.py deleted file mode 100644 index 47c8aae7..00000000 --- a/src/kernel/types/shared/error.py +++ /dev/null @@ -1,21 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Optional - -from ..._models import BaseModel -from .error_detail import ErrorDetail - -__all__ = ["Error"] - - -class Error(BaseModel): - code: str - """Application-specific error code (machine-readable)""" - - message: str - """Human-readable error description for debugging""" - - details: Optional[List[ErrorDetail]] = None - """Additional error details (for multiple errors)""" - - inner_error: Optional[ErrorDetail] = None diff --git a/src/kernel/types/shared/error_event.py b/src/kernel/types/shared/error_event.py index 542634b2..172a8beb 100644 --- a/src/kernel/types/shared/error_event.py +++ b/src/kernel/types/shared/error_event.py @@ -1,12 +1,26 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from typing import List, Optional from datetime import datetime from typing_extensions import Literal -from .error import Error from ..._models import BaseModel +from .error_detail import ErrorDetail -__all__ = ["ErrorEvent"] +__all__ = ["ErrorEvent", "Error"] + + +class Error(BaseModel): + code: str + """Application-specific error code (machine-readable)""" + + message: str + """Human-readable error description for debugging""" + + details: Optional[List[ErrorDetail]] = None + """Additional error details (for multiple errors)""" + + inner_error: Optional[ErrorDetail] = None class ErrorEvent(BaseModel): From ff648444f56858f40ff2faa113a2e06d579eba40 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 15:25:57 +0000 Subject: [PATCH 091/448] feat(api): update via SDK Studio --- .stats.yml | 2 +- api.md | 2 +- src/kernel/types/__init__.py | 2 +- src/kernel/types/shared/__init__.py | 1 + src/kernel/types/shared/error_event.py | 20 +++----------------- src/kernel/types/shared/error_model.py | 21 +++++++++++++++++++++ 6 files changed, 28 insertions(+), 20 deletions(-) create mode 100644 src/kernel/types/shared/error_model.py diff --git a/.stats.yml b/.stats.yml index 71dae95c..ba1c7c95 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 15 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-5d4e11bc46eeecee7363d56a9dfe946acee997d5b352c2b0a50c20e742c54d2d.yml openapi_spec_hash: 333e53ad9c706296b9afdb8ff73bec8f -config_hash: 79af9b3bec53ee798dddcf815befa25d +config_hash: 0fdf285ddd8dee229fd84ea57df9080f diff --git a/api.md b/api.md index 4574ce71..cb25dcbe 100644 --- a/api.md +++ b/api.md @@ -1,7 +1,7 @@ # Shared Types ```python -from kernel.types import ErrorDetail, ErrorEvent, LogEvent +from kernel.types import ErrorDetail, ErrorEvent, ErrorModel, LogEvent ``` # Deployments diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 87e9ac8e..cf2dbbc0 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations -from .shared import LogEvent as LogEvent, ErrorEvent as ErrorEvent, ErrorDetail as ErrorDetail +from .shared import LogEvent as LogEvent, ErrorEvent as ErrorEvent, ErrorModel as ErrorModel, ErrorDetail as ErrorDetail from .app_list_params import AppListParams as AppListParams from .app_list_response import AppListResponse as AppListResponse from .browser_persistence import BrowserPersistence as BrowserPersistence diff --git a/src/kernel/types/shared/__init__.py b/src/kernel/types/shared/__init__.py index 45c73b34..e444e22b 100644 --- a/src/kernel/types/shared/__init__.py +++ b/src/kernel/types/shared/__init__.py @@ -2,4 +2,5 @@ from .log_event import LogEvent as LogEvent from .error_event import ErrorEvent as ErrorEvent +from .error_model import ErrorModel as ErrorModel from .error_detail import ErrorDetail as ErrorDetail diff --git a/src/kernel/types/shared/error_event.py b/src/kernel/types/shared/error_event.py index 172a8beb..0041b899 100644 --- a/src/kernel/types/shared/error_event.py +++ b/src/kernel/types/shared/error_event.py @@ -1,30 +1,16 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List, Optional from datetime import datetime from typing_extensions import Literal from ..._models import BaseModel -from .error_detail import ErrorDetail +from .error_model import ErrorModel -__all__ = ["ErrorEvent", "Error"] - - -class Error(BaseModel): - code: str - """Application-specific error code (machine-readable)""" - - message: str - """Human-readable error description for debugging""" - - details: Optional[List[ErrorDetail]] = None - """Additional error details (for multiple errors)""" - - inner_error: Optional[ErrorDetail] = None +__all__ = ["ErrorEvent"] class ErrorEvent(BaseModel): - error: Error + error: ErrorModel event: Literal["error"] """Event type identifier (always "error").""" diff --git a/src/kernel/types/shared/error_model.py b/src/kernel/types/shared/error_model.py new file mode 100644 index 00000000..6cb4811c --- /dev/null +++ b/src/kernel/types/shared/error_model.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from ..._models import BaseModel +from .error_detail import ErrorDetail + +__all__ = ["ErrorModel"] + + +class ErrorModel(BaseModel): + code: str + """Application-specific error code (machine-readable)""" + + message: str + """Human-readable error description for debugging""" + + details: Optional[List[ErrorDetail]] = None + """Additional error details (for multiple errors)""" + + inner_error: Optional[ErrorDetail] = None From 984c77b4d0b69b3bec95cc328e0a177504002a30 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 02:12:12 +0000 Subject: [PATCH 092/448] chore(readme): update badges --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9d3c4fa3..fa501abe 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Kernel Python API library -[![PyPI version](https://img.shields.io/pypi/v/kernel.svg)](https://pypi.org/project/kernel/) +[![PyPI version]()](https://pypi.org/project/kernel/) The Kernel Python library provides convenient access to the Kernel REST API from any Python 3.8+ application. The library includes type definitions for all request params and response fields, From 99c270fd99ce20a6c15b9390ffd7771a579d76bc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 05:49:09 +0000 Subject: [PATCH 093/448] fix(tests): fix: tests which call HTTP endpoints directly with the example parameters --- tests/test_client.py | 69 ++++++++------------------------------------ 1 file changed, 12 insertions(+), 57 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 25c8f4ac..11e8a84f 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -23,9 +23,7 @@ from kernel import Kernel, AsyncKernel, APIResponseValidationError from kernel._types import Omit -from kernel._utils import maybe_transform from kernel._models import BaseModel, FinalRequestOptions -from kernel._constants import RAW_RESPONSE_HEADER from kernel._exceptions import KernelError, APIStatusError, APITimeoutError, APIResponseValidationError from kernel._base_client import ( DEFAULT_TIMEOUT, @@ -35,7 +33,6 @@ DefaultAsyncHttpxClient, make_request_options, ) -from kernel.types.browser_create_params import BrowserCreateParams from .utils import update_env @@ -721,44 +718,21 @@ def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str @mock.patch("kernel._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: + def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: Kernel) -> None: respx_mock.post("/browsers").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - self.client.post( - "/browsers", - body=cast( - object, - maybe_transform( - dict(invocation_id="REPLACE_ME", persistence={"id": "browser-for-user-1234"}), - BrowserCreateParams, - ), - ), - cast_to=httpx.Response, - options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, - ) + client.browsers.with_streaming_response.create(invocation_id="rr33xuugxj9h0bkf1rdt2bet").__enter__() assert _get_open_connections(self.client) == 0 @mock.patch("kernel._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: + def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: Kernel) -> None: respx_mock.post("/browsers").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - self.client.post( - "/browsers", - body=cast( - object, - maybe_transform( - dict(invocation_id="REPLACE_ME", persistence={"id": "browser-for-user-1234"}), - BrowserCreateParams, - ), - ), - cast_to=httpx.Response, - options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, - ) - + client.browsers.with_streaming_response.create(invocation_id="rr33xuugxj9h0bkf1rdt2bet").__enter__() assert _get_open_connections(self.client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -1572,44 +1546,25 @@ async def test_parse_retry_after_header(self, remaining_retries: int, retry_afte @mock.patch("kernel._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: + async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, async_client: AsyncKernel) -> None: respx_mock.post("/browsers").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - await self.client.post( - "/browsers", - body=cast( - object, - maybe_transform( - dict(invocation_id="REPLACE_ME", persistence={"id": "browser-for-user-1234"}), - BrowserCreateParams, - ), - ), - cast_to=httpx.Response, - options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, - ) + await async_client.browsers.with_streaming_response.create( + invocation_id="rr33xuugxj9h0bkf1rdt2bet" + ).__aenter__() assert _get_open_connections(self.client) == 0 @mock.patch("kernel._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: + async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, async_client: AsyncKernel) -> None: respx_mock.post("/browsers").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - await self.client.post( - "/browsers", - body=cast( - object, - maybe_transform( - dict(invocation_id="REPLACE_ME", persistence={"id": "browser-for-user-1234"}), - BrowserCreateParams, - ), - ), - cast_to=httpx.Response, - options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, - ) - + await async_client.browsers.with_streaming_response.create( + invocation_id="rr33xuugxj9h0bkf1rdt2bet" + ).__aenter__() assert _get_open_connections(self.client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) From a2b3407e7fa50db4cb594295976826586bd332f5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 10:35:55 +0000 Subject: [PATCH 094/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 2aca35ae..4208b5cb 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.5.0" + ".": "0.6.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index ce8b48d2..3bfd2977 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.5.0" +version = "0.6.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 2c947c24..c6e626d2 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.5.0" # x-release-please-version +__version__ = "0.6.0" # x-release-please-version From 7177ddd35fdd413e525b057735ee3807eb3eedde Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 17:58:57 +0000 Subject: [PATCH 095/448] feat(api): add delete_browsers endpoint --- .stats.yml | 8 +-- api.md | 1 + src/kernel/resources/invocations.py | 82 +++++++++++++++++++++++- tests/api_resources/test_invocations.py | 84 +++++++++++++++++++++++++ 4 files changed, 170 insertions(+), 5 deletions(-) diff --git a/.stats.yml b/.stats.yml index ba1c7c95..4a84456e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 15 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-5d4e11bc46eeecee7363d56a9dfe946acee997d5b352c2b0a50c20e742c54d2d.yml -openapi_spec_hash: 333e53ad9c706296b9afdb8ff73bec8f -config_hash: 0fdf285ddd8dee229fd84ea57df9080f +configured_endpoints: 16 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-b019e469425a59061f37c5fdc7a131a5291c66134ef0627db4f06bb1f4af0b15.yml +openapi_spec_hash: f66a3c2efddb168db9539ba2507b10b8 +config_hash: aae6721b2be9ec8565dfc8f7eadfe105 diff --git a/api.md b/api.md index cb25dcbe..0127e610 100644 --- a/api.md +++ b/api.md @@ -67,6 +67,7 @@ Methods: - client.invocations.create(\*\*params) -> InvocationCreateResponse - client.invocations.retrieve(id) -> InvocationRetrieveResponse - client.invocations.update(id, \*\*params) -> InvocationUpdateResponse +- client.invocations.delete_browsers(id) -> None - client.invocations.follow(id) -> InvocationFollowResponse # Browsers diff --git a/src/kernel/resources/invocations.py b/src/kernel/resources/invocations.py index c87b8d79..3de46d0b 100644 --- a/src/kernel/resources/invocations.py +++ b/src/kernel/resources/invocations.py @@ -8,7 +8,7 @@ import httpx from ..types import invocation_create_params, invocation_update_params -from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -183,6 +183,40 @@ def update( cast_to=InvocationUpdateResponse, ) + def delete_browsers( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """ + Delete all browser sessions created within the specified invocation. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + f"/invocations/{id}/browsers", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + def follow( self, id: str, @@ -379,6 +413,40 @@ async def update( cast_to=InvocationUpdateResponse, ) + async def delete_browsers( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """ + Delete all browser sessions created within the specified invocation. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + f"/invocations/{id}/browsers", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + async def follow( self, id: str, @@ -433,6 +501,9 @@ def __init__(self, invocations: InvocationsResource) -> None: self.update = to_raw_response_wrapper( invocations.update, ) + self.delete_browsers = to_raw_response_wrapper( + invocations.delete_browsers, + ) self.follow = to_raw_response_wrapper( invocations.follow, ) @@ -451,6 +522,9 @@ def __init__(self, invocations: AsyncInvocationsResource) -> None: self.update = async_to_raw_response_wrapper( invocations.update, ) + self.delete_browsers = async_to_raw_response_wrapper( + invocations.delete_browsers, + ) self.follow = async_to_raw_response_wrapper( invocations.follow, ) @@ -469,6 +543,9 @@ def __init__(self, invocations: InvocationsResource) -> None: self.update = to_streamed_response_wrapper( invocations.update, ) + self.delete_browsers = to_streamed_response_wrapper( + invocations.delete_browsers, + ) self.follow = to_streamed_response_wrapper( invocations.follow, ) @@ -487,6 +564,9 @@ def __init__(self, invocations: AsyncInvocationsResource) -> None: self.update = async_to_streamed_response_wrapper( invocations.update, ) + self.delete_browsers = async_to_streamed_response_wrapper( + invocations.delete_browsers, + ) self.follow = async_to_streamed_response_wrapper( invocations.follow, ) diff --git a/tests/api_resources/test_invocations.py b/tests/api_resources/test_invocations.py index c11ea7c8..4fbd4606 100644 --- a/tests/api_resources/test_invocations.py +++ b/tests/api_resources/test_invocations.py @@ -171,6 +171,48 @@ def test_path_params_update(self, client: Kernel) -> None: status="succeeded", ) + @pytest.mark.skip() + @parametrize + def test_method_delete_browsers(self, client: Kernel) -> None: + invocation = client.invocations.delete_browsers( + "id", + ) + assert invocation is None + + @pytest.mark.skip() + @parametrize + def test_raw_response_delete_browsers(self, client: Kernel) -> None: + response = client.invocations.with_raw_response.delete_browsers( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + invocation = response.parse() + assert invocation is None + + @pytest.mark.skip() + @parametrize + def test_streaming_response_delete_browsers(self, client: Kernel) -> None: + with client.invocations.with_streaming_response.delete_browsers( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + invocation = response.parse() + assert invocation is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + def test_path_params_delete_browsers(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.invocations.with_raw_response.delete_browsers( + "", + ) + @pytest.mark.skip( reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" ) @@ -374,6 +416,48 @@ async def test_path_params_update(self, async_client: AsyncKernel) -> None: status="succeeded", ) + @pytest.mark.skip() + @parametrize + async def test_method_delete_browsers(self, async_client: AsyncKernel) -> None: + invocation = await async_client.invocations.delete_browsers( + "id", + ) + assert invocation is None + + @pytest.mark.skip() + @parametrize + async def test_raw_response_delete_browsers(self, async_client: AsyncKernel) -> None: + response = await async_client.invocations.with_raw_response.delete_browsers( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + invocation = await response.parse() + assert invocation is None + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_delete_browsers(self, async_client: AsyncKernel) -> None: + async with async_client.invocations.with_streaming_response.delete_browsers( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + invocation = await response.parse() + assert invocation is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + async def test_path_params_delete_browsers(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.invocations.with_raw_response.delete_browsers( + "", + ) + @pytest.mark.skip( reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" ) From 751d5eee63baafde19d2790fc93c6789e87e9f21 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 18:02:25 +0000 Subject: [PATCH 096/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 4208b5cb..ac031714 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.6.0" + ".": "0.6.1" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 3bfd2977..e783ee36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.6.0" +version = "0.6.1" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index c6e626d2..23bc5760 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.6.0" # x-release-please-version +__version__ = "0.6.1" # x-release-please-version From 68c883b68ab4596f8206298be296a6ea83df01c6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 19 Jun 2025 02:50:43 +0000 Subject: [PATCH 097/448] docs(client): fix httpx.Timeout documentation reference --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fa501abe..d5f8c84d 100644 --- a/README.md +++ b/README.md @@ -192,7 +192,7 @@ client.with_options(max_retries=5).browsers.create( ### Timeouts By default requests time out after 1 minute. You can configure this with a `timeout` option, -which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/#fine-tuning-the-configuration) object: +which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/timeouts/#fine-tuning-the-configuration) object: ```python from kernel import Kernel From 79db3721e96980a6c797ad48a1f99cc018c6be62 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 21 Jun 2025 04:08:09 +0000 Subject: [PATCH 098/448] feat(client): add support for aiohttp --- README.md | 37 +++++++++++++++++ pyproject.toml | 2 + requirements-dev.lock | 27 ++++++++++++ requirements.lock | 27 ++++++++++++ src/kernel/__init__.py | 3 +- src/kernel/_base_client.py | 22 ++++++++++ tests/api_resources/apps/test_deployments.py | 4 +- tests/api_resources/test_apps.py | 4 +- tests/api_resources/test_browsers.py | 4 +- tests/api_resources/test_deployments.py | 4 +- tests/api_resources/test_invocations.py | 4 +- tests/conftest.py | 43 +++++++++++++++++--- 12 files changed, 169 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index d5f8c84d..00a4d1b0 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,43 @@ asyncio.run(main()) Functionality between the synchronous and asynchronous clients is otherwise identical. +### With aiohttp + +By default, the async client uses `httpx` for HTTP requests. However, for improved concurrency performance you may also use `aiohttp` as the HTTP backend. + +You can enable this by installing `aiohttp`: + +```sh +# install from PyPI +pip install kernel[aiohttp] +``` + +Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: + +```python +import os +import asyncio +from kernel import DefaultAioHttpClient +from kernel import AsyncKernel + + +async def main() -> None: + async with AsyncKernel( + api_key=os.environ.get("KERNEL_API_KEY"), # This is the default and can be omitted + http_client=DefaultAioHttpClient(), + ) as client: + deployment = await client.apps.deployments.create( + entrypoint_rel_path="main.ts", + file=b"REPLACE_ME", + env_vars={"OPENAI_API_KEY": "x"}, + version="1.0.0", + ) + print(deployment.apps) + + +asyncio.run(main()) +``` + ## Using types Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev) which also provide helper methods for things like: diff --git a/pyproject.toml b/pyproject.toml index e783ee36..8e5e257c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,8 @@ classifiers = [ Homepage = "https://github.com/onkernel/kernel-python-sdk" Repository = "https://github.com/onkernel/kernel-python-sdk" +[project.optional-dependencies] +aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.6"] [tool.rye] managed = true diff --git a/requirements-dev.lock b/requirements-dev.lock index f40d9851..27de013f 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -10,6 +10,13 @@ # universal: false -e file:. +aiohappyeyeballs==2.6.1 + # via aiohttp +aiohttp==3.12.8 + # via httpx-aiohttp + # via kernel +aiosignal==1.3.2 + # via aiohttp annotated-types==0.6.0 # via pydantic anyio==4.4.0 @@ -17,6 +24,10 @@ anyio==4.4.0 # via kernel argcomplete==3.1.2 # via nox +async-timeout==5.0.1 + # via aiohttp +attrs==25.3.0 + # via aiohttp certifi==2023.7.22 # via httpcore # via httpx @@ -34,16 +45,23 @@ execnet==2.1.1 # via pytest-xdist filelock==3.12.4 # via virtualenv +frozenlist==1.6.2 + # via aiohttp + # via aiosignal h11==0.14.0 # via httpcore httpcore==1.0.2 # via httpx httpx==0.28.1 + # via httpx-aiohttp # via kernel # via respx +httpx-aiohttp==0.1.6 + # via kernel idna==3.4 # via anyio # via httpx + # via yarl importlib-metadata==7.0.0 iniconfig==2.0.0 # via pytest @@ -51,6 +69,9 @@ markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py +multidict==6.4.4 + # via aiohttp + # via yarl mypy==1.14.1 mypy-extensions==1.0.0 # via mypy @@ -65,6 +86,9 @@ platformdirs==3.11.0 # via virtualenv pluggy==1.5.0 # via pytest +propcache==0.3.1 + # via aiohttp + # via yarl pydantic==2.10.3 # via kernel pydantic-core==2.27.1 @@ -98,11 +122,14 @@ tomli==2.0.2 typing-extensions==4.12.2 # via anyio # via kernel + # via multidict # via mypy # via pydantic # via pydantic-core # via pyright virtualenv==20.24.5 # via nox +yarl==1.20.0 + # via aiohttp zipp==3.17.0 # via importlib-metadata diff --git a/requirements.lock b/requirements.lock index 40719199..4006aa29 100644 --- a/requirements.lock +++ b/requirements.lock @@ -10,11 +10,22 @@ # universal: false -e file:. +aiohappyeyeballs==2.6.1 + # via aiohttp +aiohttp==3.12.8 + # via httpx-aiohttp + # via kernel +aiosignal==1.3.2 + # via aiohttp annotated-types==0.6.0 # via pydantic anyio==4.4.0 # via httpx # via kernel +async-timeout==5.0.1 + # via aiohttp +attrs==25.3.0 + # via aiohttp certifi==2023.7.22 # via httpcore # via httpx @@ -22,15 +33,28 @@ distro==1.8.0 # via kernel exceptiongroup==1.2.2 # via anyio +frozenlist==1.6.2 + # via aiohttp + # via aiosignal h11==0.14.0 # via httpcore httpcore==1.0.2 # via httpx httpx==0.28.1 + # via httpx-aiohttp + # via kernel +httpx-aiohttp==0.1.6 # via kernel 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 kernel pydantic-core==2.27.1 @@ -41,5 +65,8 @@ sniffio==1.3.0 typing-extensions==4.12.2 # via anyio # via kernel + # via multidict # via pydantic # via pydantic-core +yarl==1.20.0 + # via aiohttp diff --git a/src/kernel/__init__.py b/src/kernel/__init__.py index 4c0f2540..4ad2b380 100644 --- a/src/kernel/__init__.py +++ b/src/kernel/__init__.py @@ -37,7 +37,7 @@ UnprocessableEntityError, APIResponseValidationError, ) -from ._base_client import DefaultHttpxClient, DefaultAsyncHttpxClient +from ._base_client import DefaultHttpxClient, DefaultAioHttpClient, DefaultAsyncHttpxClient from ._utils._logs import setup_logging as _setup_logging __all__ = [ @@ -80,6 +80,7 @@ "DEFAULT_CONNECTION_LIMITS", "DefaultHttpxClient", "DefaultAsyncHttpxClient", + "DefaultAioHttpClient", ] if not _t.TYPE_CHECKING: diff --git a/src/kernel/_base_client.py b/src/kernel/_base_client.py index c86e919f..c90f227a 100644 --- a/src/kernel/_base_client.py +++ b/src/kernel/_base_client.py @@ -1289,6 +1289,24 @@ def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) +try: + import httpx_aiohttp +except ImportError: + + class _DefaultAioHttpClient(httpx.AsyncClient): + def __init__(self, **_kwargs: Any) -> None: + raise RuntimeError("To use the aiohttp client you must have installed the package with the `aiohttp` extra") +else: + + class _DefaultAioHttpClient(httpx_aiohttp.HttpxAiohttpClient): # type: ignore + def __init__(self, **kwargs: Any) -> None: + kwargs.setdefault("timeout", DEFAULT_TIMEOUT) + kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) + kwargs.setdefault("follow_redirects", True) + + super().__init__(**kwargs) + + if TYPE_CHECKING: DefaultAsyncHttpxClient = httpx.AsyncClient """An alias to `httpx.AsyncClient` that provides the same defaults that this SDK @@ -1297,8 +1315,12 @@ def __init__(self, **kwargs: Any) -> None: This is useful because overriding the `http_client` with your own instance of `httpx.AsyncClient` will result in httpx's defaults being used, not ours. """ + + DefaultAioHttpClient = httpx.AsyncClient + """An alias to `httpx.AsyncClient` that changes the default HTTP transport to `aiohttp`.""" else: DefaultAsyncHttpxClient = _DefaultAsyncHttpxClient + DefaultAioHttpClient = _DefaultAioHttpClient class AsyncHttpxClientWrapper(DefaultAsyncHttpxClient): diff --git a/tests/api_resources/apps/test_deployments.py b/tests/api_resources/apps/test_deployments.py index 0a93c44c..7b613c85 100644 --- a/tests/api_resources/apps/test_deployments.py +++ b/tests/api_resources/apps/test_deployments.py @@ -118,7 +118,9 @@ def test_path_params_follow(self, client: Kernel) -> None: class TestAsyncDeployments: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @pytest.mark.skip() @parametrize diff --git a/tests/api_resources/test_apps.py b/tests/api_resources/test_apps.py index b902576a..05066cdd 100644 --- a/tests/api_resources/test_apps.py +++ b/tests/api_resources/test_apps.py @@ -56,7 +56,9 @@ def test_streaming_response_list(self, client: Kernel) -> None: class TestAsyncApps: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @pytest.mark.skip() @parametrize diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index 4593d2fb..91a9429e 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -213,7 +213,9 @@ def test_path_params_delete_by_id(self, client: Kernel) -> None: class TestAsyncBrowsers: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @pytest.mark.skip() @parametrize diff --git a/tests/api_resources/test_deployments.py b/tests/api_resources/test_deployments.py index 4bd80fca..954bc94a 100644 --- a/tests/api_resources/test_deployments.py +++ b/tests/api_resources/test_deployments.py @@ -160,7 +160,9 @@ def test_path_params_follow(self, client: Kernel) -> None: class TestAsyncDeployments: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @pytest.mark.skip() @parametrize diff --git a/tests/api_resources/test_invocations.py b/tests/api_resources/test_invocations.py index 4fbd4606..e739e447 100644 --- a/tests/api_resources/test_invocations.py +++ b/tests/api_resources/test_invocations.py @@ -264,7 +264,9 @@ def test_path_params_follow(self, client: Kernel) -> None: class TestAsyncInvocations: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @pytest.mark.skip() @parametrize diff --git a/tests/conftest.py b/tests/conftest.py index 3a11d3f1..c860af02 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,10 +6,12 @@ import logging from typing import TYPE_CHECKING, Iterator, AsyncIterator +import httpx import pytest from pytest_asyncio import is_async_test -from kernel import Kernel, AsyncKernel +from kernel import Kernel, AsyncKernel, DefaultAioHttpClient +from kernel._utils import is_dict if TYPE_CHECKING: from _pytest.fixtures import FixtureRequest # pyright: ignore[reportPrivateImportUsage] @@ -27,6 +29,19 @@ def pytest_collection_modifyitems(items: list[pytest.Function]) -> None: for async_test in pytest_asyncio_tests: async_test.add_marker(session_scope_marker, append=False) + # We skip tests that use both the aiohttp client and respx_mock as respx_mock + # doesn't support custom transports. + for item in items: + if "async_client" not in item.fixturenames or "respx_mock" not in item.fixturenames: + continue + + if not hasattr(item, "callspec"): + continue + + async_client_param = item.callspec.params.get("async_client") + if is_dict(async_client_param) and async_client_param.get("http_client") == "aiohttp": + item.add_marker(pytest.mark.skip(reason="aiohttp client is not compatible with respx_mock")) + base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -45,9 +60,25 @@ def client(request: FixtureRequest) -> Iterator[Kernel]: @pytest.fixture(scope="session") async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncKernel]: - strict = getattr(request, "param", True) - if not isinstance(strict, bool): - raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") - - async with AsyncKernel(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client: + param = getattr(request, "param", True) + + # defaults + strict = True + http_client: None | httpx.AsyncClient = None + + if isinstance(param, bool): + strict = param + elif is_dict(param): + strict = param.get("strict", True) + assert isinstance(strict, bool) + + http_client_type = param.get("http_client", "httpx") + if http_client_type == "aiohttp": + http_client = DefaultAioHttpClient() + else: + raise TypeError(f"Unexpected fixture parameter type {type(param)}, expected bool or dict") + + async with AsyncKernel( + base_url=base_url, api_key=api_key, _strict_response_validation=strict, http_client=http_client + ) as client: yield client From 1fbf44d915bdb709f5c6ab2f7a19a2159d522339 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Jun 2025 04:29:46 +0000 Subject: [PATCH 099/448] chore(tests): skip some failing tests on the latest python versions --- tests/test_client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index 11e8a84f..1c1160e8 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -191,6 +191,7 @@ def test_copy_signature(self) -> None: copy_param = copy_signature.parameters.get(name) assert copy_param is not None, f"copy() signature is missing the {name} param" + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") def test_copy_build_request(self) -> None: options = FinalRequestOptions(method="get", url="/foo") @@ -1001,6 +1002,7 @@ def test_copy_signature(self) -> None: copy_param = copy_signature.parameters.get(name) assert copy_param is not None, f"copy() signature is missing the {name} param" + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") def test_copy_build_request(self) -> None: options = FinalRequestOptions(method="get", url="/foo") From 087d9169190abc7c86a071dadcf0ca9764f4b5e6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Jun 2025 19:49:45 +0000 Subject: [PATCH 100/448] feat(api): add `since` parameter to deployment logs endpoint --- .stats.yml | 6 +-- api.md | 4 +- src/kernel/resources/deployments.py | 20 +++++++-- src/kernel/types/__init__.py | 9 +++- src/kernel/types/app_list_response.py | 3 ++ .../types/apps/deployment_follow_response.py | 3 +- src/kernel/types/deployment_follow_params.py | 12 ++++++ .../types/deployment_follow_response.py | 4 +- .../types/invocation_follow_response.py | 3 +- src/kernel/types/shared/__init__.py | 1 + src/kernel/types/shared/heartbeat_event.py | 16 +++++++ tests/api_resources/test_deployments.py | 43 +++++++++++++++---- 12 files changed, 103 insertions(+), 21 deletions(-) create mode 100644 src/kernel/types/deployment_follow_params.py create mode 100644 src/kernel/types/shared/heartbeat_event.py diff --git a/.stats.yml b/.stats.yml index 4a84456e..b296d07c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 16 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-b019e469425a59061f37c5fdc7a131a5291c66134ef0627db4f06bb1f4af0b15.yml -openapi_spec_hash: f66a3c2efddb168db9539ba2507b10b8 -config_hash: aae6721b2be9ec8565dfc8f7eadfe105 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2aec229ccf91f7c1ac95aa675ea2a59bd61af9e363a22c3b49677992f1eeb16a.yml +openapi_spec_hash: c80cd5d52a79cd5366a76d4a825bd27a +config_hash: b8e1fff080fbaa22656ab0a57b591777 diff --git a/api.md b/api.md index 0127e610..c8f114f3 100644 --- a/api.md +++ b/api.md @@ -1,7 +1,7 @@ # Shared Types ```python -from kernel.types import ErrorDetail, ErrorEvent, ErrorModel, LogEvent +from kernel.types import ErrorDetail, ErrorEvent, ErrorModel, HeartbeatEvent, LogEvent ``` # Deployments @@ -21,7 +21,7 @@ Methods: - client.deployments.create(\*\*params) -> DeploymentCreateResponse - client.deployments.retrieve(id) -> DeploymentRetrieveResponse -- client.deployments.follow(id) -> DeploymentFollowResponse +- client.deployments.follow(id, \*\*params) -> DeploymentFollowResponse # Apps diff --git a/src/kernel/resources/deployments.py b/src/kernel/resources/deployments.py index 6442ff0e..f27c894d 100644 --- a/src/kernel/resources/deployments.py +++ b/src/kernel/resources/deployments.py @@ -7,7 +7,7 @@ import httpx -from ..types import deployment_create_params +from ..types import deployment_create_params, deployment_follow_params from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform from .._compat import cached_property @@ -150,6 +150,7 @@ def follow( self, id: str, *, + since: 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, @@ -163,6 +164,8 @@ def follow( deployment reaches a terminal state. Args: + since: Show logs since the given time (RFC timestamps or durations like 5m). + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -177,7 +180,11 @@ def follow( return self._get( f"/deployments/{id}/events", options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform({"since": since}, deployment_follow_params.DeploymentFollowParams), ), cast_to=cast( Any, DeploymentFollowResponse @@ -310,6 +317,7 @@ async def follow( self, id: str, *, + since: 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, @@ -323,6 +331,8 @@ async def follow( deployment reaches a terminal state. Args: + since: Show logs since the given time (RFC timestamps or durations like 5m). + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -337,7 +347,11 @@ async def follow( return await self._get( f"/deployments/{id}/events", options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform({"since": since}, deployment_follow_params.DeploymentFollowParams), ), cast_to=cast( Any, DeploymentFollowResponse diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index cf2dbbc0..c816cf1f 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -2,7 +2,13 @@ from __future__ import annotations -from .shared import LogEvent as LogEvent, ErrorEvent as ErrorEvent, ErrorModel as ErrorModel, ErrorDetail as ErrorDetail +from .shared import ( + LogEvent as LogEvent, + ErrorEvent as ErrorEvent, + ErrorModel as ErrorModel, + ErrorDetail as ErrorDetail, + HeartbeatEvent as HeartbeatEvent, +) from .app_list_params import AppListParams as AppListParams from .app_list_response import AppListResponse as AppListResponse from .browser_persistence import BrowserPersistence as BrowserPersistence @@ -13,6 +19,7 @@ from .invocation_state_event import InvocationStateEvent as InvocationStateEvent from .browser_create_response import BrowserCreateResponse as BrowserCreateResponse from .deployment_create_params import DeploymentCreateParams as DeploymentCreateParams +from .deployment_follow_params import DeploymentFollowParams as DeploymentFollowParams from .invocation_create_params import InvocationCreateParams as InvocationCreateParams from .invocation_update_params import InvocationUpdateParams as InvocationUpdateParams from .browser_persistence_param import BrowserPersistenceParam as BrowserPersistenceParam diff --git a/src/kernel/types/app_list_response.py b/src/kernel/types/app_list_response.py index 1d35fd2e..cf8463ea 100644 --- a/src/kernel/types/app_list_response.py +++ b/src/kernel/types/app_list_response.py @@ -15,6 +15,9 @@ class AppListResponseItem(BaseModel): app_name: str """Name of the application""" + deployment: str + """Deployment ID""" + region: Literal["aws.us-east-1a"] """Deployment region code""" diff --git a/src/kernel/types/apps/deployment_follow_response.py b/src/kernel/types/apps/deployment_follow_response.py index fae1b2b4..6a196961 100644 --- a/src/kernel/types/apps/deployment_follow_response.py +++ b/src/kernel/types/apps/deployment_follow_response.py @@ -7,6 +7,7 @@ from ..._utils import PropertyInfo from ..._models import BaseModel from ..shared.log_event import LogEvent +from ..shared.heartbeat_event import HeartbeatEvent __all__ = ["DeploymentFollowResponse", "StateEvent", "StateUpdateEvent"] @@ -36,5 +37,5 @@ class StateUpdateEvent(BaseModel): DeploymentFollowResponse: TypeAlias = Annotated[ - Union[StateEvent, StateUpdateEvent, LogEvent], PropertyInfo(discriminator="event") + Union[StateEvent, StateUpdateEvent, LogEvent, HeartbeatEvent], PropertyInfo(discriminator="event") ] diff --git a/src/kernel/types/deployment_follow_params.py b/src/kernel/types/deployment_follow_params.py new file mode 100644 index 00000000..861f161e --- /dev/null +++ b/src/kernel/types/deployment_follow_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["DeploymentFollowParams"] + + +class DeploymentFollowParams(TypedDict, total=False): + since: str + """Show logs since the given time (RFC timestamps or durations like 5m).""" diff --git a/src/kernel/types/deployment_follow_response.py b/src/kernel/types/deployment_follow_response.py index e95ce262..00f06856 100644 --- a/src/kernel/types/deployment_follow_response.py +++ b/src/kernel/types/deployment_follow_response.py @@ -9,6 +9,7 @@ from .shared.log_event import LogEvent from .shared.error_event import ErrorEvent from .deployment_state_event import DeploymentStateEvent +from .shared.heartbeat_event import HeartbeatEvent __all__ = ["DeploymentFollowResponse", "AppVersionSummaryEvent", "AppVersionSummaryEventAction"] @@ -45,5 +46,6 @@ class AppVersionSummaryEvent(BaseModel): DeploymentFollowResponse: TypeAlias = Annotated[ - Union[LogEvent, DeploymentStateEvent, AppVersionSummaryEvent, ErrorEvent], PropertyInfo(discriminator="event") + Union[LogEvent, DeploymentStateEvent, AppVersionSummaryEvent, ErrorEvent, HeartbeatEvent], + PropertyInfo(discriminator="event"), ] diff --git a/src/kernel/types/invocation_follow_response.py b/src/kernel/types/invocation_follow_response.py index e3d7e8ef..2effbde7 100644 --- a/src/kernel/types/invocation_follow_response.py +++ b/src/kernel/types/invocation_follow_response.py @@ -7,9 +7,10 @@ from .shared.log_event import LogEvent from .shared.error_event import ErrorEvent from .invocation_state_event import InvocationStateEvent +from .shared.heartbeat_event import HeartbeatEvent __all__ = ["InvocationFollowResponse"] InvocationFollowResponse: TypeAlias = Annotated[ - Union[LogEvent, InvocationStateEvent, ErrorEvent], PropertyInfo(discriminator="event") + Union[LogEvent, InvocationStateEvent, ErrorEvent, HeartbeatEvent], PropertyInfo(discriminator="event") ] diff --git a/src/kernel/types/shared/__init__.py b/src/kernel/types/shared/__init__.py index e444e22b..60a8de89 100644 --- a/src/kernel/types/shared/__init__.py +++ b/src/kernel/types/shared/__init__.py @@ -4,3 +4,4 @@ from .error_event import ErrorEvent as ErrorEvent from .error_model import ErrorModel as ErrorModel from .error_detail import ErrorDetail as ErrorDetail +from .heartbeat_event import HeartbeatEvent as HeartbeatEvent diff --git a/src/kernel/types/shared/heartbeat_event.py b/src/kernel/types/shared/heartbeat_event.py new file mode 100644 index 00000000..d5ca811f --- /dev/null +++ b/src/kernel/types/shared/heartbeat_event.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from datetime import datetime +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["HeartbeatEvent"] + + +class HeartbeatEvent(BaseModel): + event: Literal["sse_heartbeat"] + """Event type identifier (always "sse_heartbeat").""" + + timestamp: datetime + """Time the heartbeat was sent.""" diff --git a/tests/api_resources/test_deployments.py b/tests/api_resources/test_deployments.py index 954bc94a..f68c9b04 100644 --- a/tests/api_resources/test_deployments.py +++ b/tests/api_resources/test_deployments.py @@ -9,7 +9,10 @@ from kernel import Kernel, AsyncKernel from tests.utils import assert_matches_type -from kernel.types import DeploymentCreateResponse, DeploymentRetrieveResponse +from kernel.types import ( + DeploymentCreateResponse, + DeploymentRetrieveResponse, +) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -115,7 +118,18 @@ def test_path_params_retrieve(self, client: Kernel) -> None: @parametrize def test_method_follow(self, client: Kernel) -> None: deployment_stream = client.deployments.follow( - "id", + id="id", + ) + deployment_stream.response.close() + + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + def test_method_follow_with_all_params(self, client: Kernel) -> None: + deployment_stream = client.deployments.follow( + id="id", + since="2025-06-20T12:00:00Z", ) deployment_stream.response.close() @@ -125,7 +139,7 @@ def test_method_follow(self, client: Kernel) -> None: @parametrize def test_raw_response_follow(self, client: Kernel) -> None: response = client.deployments.with_raw_response.follow( - "id", + id="id", ) assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -138,7 +152,7 @@ def test_raw_response_follow(self, client: Kernel) -> None: @parametrize def test_streaming_response_follow(self, client: Kernel) -> None: with client.deployments.with_streaming_response.follow( - "id", + id="id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -155,7 +169,7 @@ def test_streaming_response_follow(self, client: Kernel) -> None: def test_path_params_follow(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): client.deployments.with_raw_response.follow( - "", + id="", ) @@ -262,7 +276,18 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: @parametrize async def test_method_follow(self, async_client: AsyncKernel) -> None: deployment_stream = await async_client.deployments.follow( - "id", + id="id", + ) + await deployment_stream.response.aclose() + + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + async def test_method_follow_with_all_params(self, async_client: AsyncKernel) -> None: + deployment_stream = await async_client.deployments.follow( + id="id", + since="2025-06-20T12:00:00Z", ) await deployment_stream.response.aclose() @@ -272,7 +297,7 @@ async def test_method_follow(self, async_client: AsyncKernel) -> None: @parametrize async def test_raw_response_follow(self, async_client: AsyncKernel) -> None: response = await async_client.deployments.with_raw_response.follow( - "id", + id="id", ) assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -285,7 +310,7 @@ async def test_raw_response_follow(self, async_client: AsyncKernel) -> None: @parametrize async def test_streaming_response_follow(self, async_client: AsyncKernel) -> None: async with async_client.deployments.with_streaming_response.follow( - "id", + id="id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -302,5 +327,5 @@ async def test_streaming_response_follow(self, async_client: AsyncKernel) -> Non async def test_path_params_follow(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): await async_client.deployments.with_raw_response.follow( - "", + id="", ) From c39bfa69f48ae7f9925e4f5b05084174c2c23b46 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Jun 2025 19:53:24 +0000 Subject: [PATCH 101/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index ac031714..e3778b2c 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.6.1" + ".": "0.6.2" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 8e5e257c..e5375bc5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.6.1" +version = "0.6.2" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 23bc5760..f175528a 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.6.1" # x-release-please-version +__version__ = "0.6.2" # x-release-please-version From 0bf38d0a6229e92aeece48e2595977379244c3ac Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Jun 2025 12:51:07 +0000 Subject: [PATCH 102/448] feat(api): /browsers no longer requires invocation id --- .stats.yml | 4 +-- README.md | 1 - src/kernel/resources/browsers.py | 4 +-- src/kernel/types/browser_create_params.py | 4 +-- tests/api_resources/test_browsers.py | 24 +++++------------ tests/test_client.py | 32 +++++++---------------- 6 files changed, 22 insertions(+), 47 deletions(-) diff --git a/.stats.yml b/.stats.yml index b296d07c..60f163b9 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 16 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2aec229ccf91f7c1ac95aa675ea2a59bd61af9e363a22c3b49677992f1eeb16a.yml -openapi_spec_hash: c80cd5d52a79cd5366a76d4a825bd27a +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-ff8ccba8b5409eaa1128df9027582cb63f66e8accd75e511f70b7c27ef26c9ae.yml +openapi_spec_hash: 1dbacc339695a7c78718f90f791d3f01 config_hash: b8e1fff080fbaa22656ab0a57b591777 diff --git a/README.md b/README.md index 00a4d1b0..8ee8a728 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,6 @@ from kernel import Kernel client = Kernel() browser = client.browsers.create( - invocation_id="rr33xuugxj9h0bkf1rdt2bet", persistence={"id": "my-awesome-browser-for-user-1234"}, ) print(browser.persistence) diff --git a/src/kernel/resources/browsers.py b/src/kernel/resources/browsers.py index e3dc8336..0cbe8201 100644 --- a/src/kernel/resources/browsers.py +++ b/src/kernel/resources/browsers.py @@ -47,7 +47,7 @@ def with_streaming_response(self) -> BrowsersResourceWithStreamingResponse: def create( self, *, - invocation_id: str, + invocation_id: str | NotGiven = NOT_GIVEN, persistence: BrowserPersistenceParam | NotGiven = NOT_GIVEN, stealth: bool | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -240,7 +240,7 @@ def with_streaming_response(self) -> AsyncBrowsersResourceWithStreamingResponse: async def create( self, *, - invocation_id: str, + invocation_id: str | NotGiven = NOT_GIVEN, persistence: BrowserPersistenceParam | NotGiven = NOT_GIVEN, stealth: bool | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index e50aefb2..2153abf9 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing_extensions import Required, TypedDict +from typing_extensions import TypedDict from .browser_persistence_param import BrowserPersistenceParam @@ -10,7 +10,7 @@ class BrowserCreateParams(TypedDict, total=False): - invocation_id: Required[str] + invocation_id: str """action invocation ID""" persistence: BrowserPersistenceParam diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index 91a9429e..f4111fa6 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -24,9 +24,7 @@ class TestBrowsers: @pytest.mark.skip() @parametrize def test_method_create(self, client: Kernel) -> None: - browser = client.browsers.create( - invocation_id="rr33xuugxj9h0bkf1rdt2bet", - ) + browser = client.browsers.create() assert_matches_type(BrowserCreateResponse, browser, path=["response"]) @pytest.mark.skip() @@ -42,9 +40,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: @pytest.mark.skip() @parametrize def test_raw_response_create(self, client: Kernel) -> None: - response = client.browsers.with_raw_response.create( - invocation_id="rr33xuugxj9h0bkf1rdt2bet", - ) + response = client.browsers.with_raw_response.create() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -54,9 +50,7 @@ def test_raw_response_create(self, client: Kernel) -> None: @pytest.mark.skip() @parametrize def test_streaming_response_create(self, client: Kernel) -> None: - with client.browsers.with_streaming_response.create( - invocation_id="rr33xuugxj9h0bkf1rdt2bet", - ) as response: + with client.browsers.with_streaming_response.create() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -220,9 +214,7 @@ class TestAsyncBrowsers: @pytest.mark.skip() @parametrize async def test_method_create(self, async_client: AsyncKernel) -> None: - browser = await async_client.browsers.create( - invocation_id="rr33xuugxj9h0bkf1rdt2bet", - ) + browser = await async_client.browsers.create() assert_matches_type(BrowserCreateResponse, browser, path=["response"]) @pytest.mark.skip() @@ -238,9 +230,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> @pytest.mark.skip() @parametrize async def test_raw_response_create(self, async_client: AsyncKernel) -> None: - response = await async_client.browsers.with_raw_response.create( - invocation_id="rr33xuugxj9h0bkf1rdt2bet", - ) + response = await async_client.browsers.with_raw_response.create() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -250,9 +240,7 @@ async def test_raw_response_create(self, async_client: AsyncKernel) -> None: @pytest.mark.skip() @parametrize async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: - async with async_client.browsers.with_streaming_response.create( - invocation_id="rr33xuugxj9h0bkf1rdt2bet", - ) as response: + async with async_client.browsers.with_streaming_response.create() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/test_client.py b/tests/test_client.py index 1c1160e8..e58799c3 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -723,7 +723,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, clien respx_mock.post("/browsers").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - client.browsers.with_streaming_response.create(invocation_id="rr33xuugxj9h0bkf1rdt2bet").__enter__() + client.browsers.with_streaming_response.create().__enter__() assert _get_open_connections(self.client) == 0 @@ -733,7 +733,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client respx_mock.post("/browsers").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - client.browsers.with_streaming_response.create(invocation_id="rr33xuugxj9h0bkf1rdt2bet").__enter__() + client.browsers.with_streaming_response.create().__enter__() assert _get_open_connections(self.client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -762,7 +762,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/browsers").mock(side_effect=retry_handler) - response = client.browsers.with_raw_response.create(invocation_id="rr33xuugxj9h0bkf1rdt2bet") + response = client.browsers.with_raw_response.create() assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -786,9 +786,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/browsers").mock(side_effect=retry_handler) - response = client.browsers.with_raw_response.create( - invocation_id="rr33xuugxj9h0bkf1rdt2bet", extra_headers={"x-stainless-retry-count": Omit()} - ) + response = client.browsers.with_raw_response.create(extra_headers={"x-stainless-retry-count": Omit()}) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -811,9 +809,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/browsers").mock(side_effect=retry_handler) - response = client.browsers.with_raw_response.create( - invocation_id="rr33xuugxj9h0bkf1rdt2bet", extra_headers={"x-stainless-retry-count": "42"} - ) + response = client.browsers.with_raw_response.create(extra_headers={"x-stainless-retry-count": "42"}) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1552,9 +1548,7 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, respx_mock.post("/browsers").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - await async_client.browsers.with_streaming_response.create( - invocation_id="rr33xuugxj9h0bkf1rdt2bet" - ).__aenter__() + await async_client.browsers.with_streaming_response.create().__aenter__() assert _get_open_connections(self.client) == 0 @@ -1564,9 +1558,7 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, respx_mock.post("/browsers").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - await async_client.browsers.with_streaming_response.create( - invocation_id="rr33xuugxj9h0bkf1rdt2bet" - ).__aenter__() + await async_client.browsers.with_streaming_response.create().__aenter__() assert _get_open_connections(self.client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -1596,7 +1588,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/browsers").mock(side_effect=retry_handler) - response = await client.browsers.with_raw_response.create(invocation_id="rr33xuugxj9h0bkf1rdt2bet") + response = await client.browsers.with_raw_response.create() assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1621,9 +1613,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/browsers").mock(side_effect=retry_handler) - response = await client.browsers.with_raw_response.create( - invocation_id="rr33xuugxj9h0bkf1rdt2bet", extra_headers={"x-stainless-retry-count": Omit()} - ) + response = await client.browsers.with_raw_response.create(extra_headers={"x-stainless-retry-count": Omit()}) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -1647,9 +1637,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/browsers").mock(side_effect=retry_handler) - response = await client.browsers.with_raw_response.create( - invocation_id="rr33xuugxj9h0bkf1rdt2bet", extra_headers={"x-stainless-retry-count": "42"} - ) + response = await client.browsers.with_raw_response.create(extra_headers={"x-stainless-retry-count": "42"}) assert response.http_request.headers.get("x-stainless-retry-count") == "42" From b24f2535fe4d683e190b81945991374c4c6a223c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 26 Jun 2025 03:20:53 +0000 Subject: [PATCH 103/448] chore(internal): codegen related update --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e3778b2c..5c87ad82 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.6.2" + ".": "0.6.3" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index e5375bc5..cec894e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.6.2" +version = "0.6.3" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index f175528a..8903bb23 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.6.2" # x-release-please-version +__version__ = "0.6.3" # x-release-please-version From 170320514f66bdb2138dae388c91efca268042c8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 27 Jun 2025 02:34:15 +0000 Subject: [PATCH 104/448] =?UTF-8?q?fix(ci):=20release-doctor=20=E2=80=94?= =?UTF-8?q?=20report=20correct=20token=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/check-release-environment | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/check-release-environment b/bin/check-release-environment index 47a8dcae..b845b0f4 100644 --- a/bin/check-release-environment +++ b/bin/check-release-environment @@ -3,7 +3,7 @@ errors=() if [ -z "${PYPI_TOKEN}" ]; then - errors+=("The KERNEL_PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") + errors+=("The PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") fi lenErrors=${#errors[@]} From 0d03c815278ee253345383a9d05db9a4ac55912e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 27 Jun 2025 17:02:22 +0000 Subject: [PATCH 105/448] feat(api): add GET deployments endpoint --- .stats.yml | 8 +- api.md | 4 +- src/kernel/resources/deployments.py | 91 ++++++++++++++++++- src/kernel/types/__init__.py | 3 + src/kernel/types/app_list_response.py | 12 ++- .../types/apps/deployment_create_response.py | 8 +- .../types/deployment_follow_response.py | 10 +- src/kernel/types/deployment_list_params.py | 12 +++ src/kernel/types/deployment_list_response.py | 38 ++++++++ src/kernel/types/shared/__init__.py | 1 + src/kernel/types/shared/app_action.py | 10 ++ tests/api_resources/test_deployments.py | 73 +++++++++++++++ 12 files changed, 247 insertions(+), 23 deletions(-) create mode 100644 src/kernel/types/deployment_list_params.py create mode 100644 src/kernel/types/deployment_list_response.py create mode 100644 src/kernel/types/shared/app_action.py diff --git a/.stats.yml b/.stats.yml index 60f163b9..9625f4ef 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 16 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-ff8ccba8b5409eaa1128df9027582cb63f66e8accd75e511f70b7c27ef26c9ae.yml -openapi_spec_hash: 1dbacc339695a7c78718f90f791d3f01 -config_hash: b8e1fff080fbaa22656ab0a57b591777 +configured_endpoints: 17 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2eeb61205775c5997abf8154cd6f6fe81a1e83870eff10050b17ed415aa7860b.yml +openapi_spec_hash: 63405add4a3f53718f8183cbb8c1a22f +config_hash: 00ec9df250b9dc077f8d3b93a442d252 diff --git a/api.md b/api.md index c8f114f3..49538b67 100644 --- a/api.md +++ b/api.md @@ -1,7 +1,7 @@ # Shared Types ```python -from kernel.types import ErrorDetail, ErrorEvent, ErrorModel, HeartbeatEvent, LogEvent +from kernel.types import AppAction, ErrorDetail, ErrorEvent, ErrorModel, HeartbeatEvent, LogEvent ``` # Deployments @@ -13,6 +13,7 @@ from kernel.types import ( DeploymentStateEvent, DeploymentCreateResponse, DeploymentRetrieveResponse, + DeploymentListResponse, DeploymentFollowResponse, ) ``` @@ -21,6 +22,7 @@ Methods: - client.deployments.create(\*\*params) -> DeploymentCreateResponse - client.deployments.retrieve(id) -> DeploymentRetrieveResponse +- client.deployments.list(\*\*params) -> DeploymentListResponse - client.deployments.follow(id, \*\*params) -> DeploymentFollowResponse # Apps diff --git a/src/kernel/resources/deployments.py b/src/kernel/resources/deployments.py index f27c894d..d54c4ec2 100644 --- a/src/kernel/resources/deployments.py +++ b/src/kernel/resources/deployments.py @@ -7,7 +7,7 @@ import httpx -from ..types import deployment_create_params, deployment_follow_params +from ..types import deployment_list_params, deployment_create_params, deployment_follow_params from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform from .._compat import cached_property @@ -20,6 +20,7 @@ ) from .._streaming import Stream, AsyncStream from .._base_client import make_request_options +from ..types.deployment_list_response import DeploymentListResponse from ..types.deployment_create_response import DeploymentCreateResponse from ..types.deployment_follow_response import DeploymentFollowResponse from ..types.deployment_retrieve_response import DeploymentRetrieveResponse @@ -146,6 +147,44 @@ def retrieve( cast_to=DeploymentRetrieveResponse, ) + def list( + self, + *, + app_name: 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, + ) -> DeploymentListResponse: + """List deployments. + + Optionally filter by application name. + + Args: + app_name: Filter results by application name. + + 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( + "/deployments", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform({"app_name": app_name}, deployment_list_params.DeploymentListParams), + ), + cast_to=DeploymentListResponse, + ) + def follow( self, id: str, @@ -313,6 +352,44 @@ async def retrieve( cast_to=DeploymentRetrieveResponse, ) + async def list( + self, + *, + app_name: 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, + ) -> DeploymentListResponse: + """List deployments. + + Optionally filter by application name. + + Args: + app_name: Filter results by application name. + + 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( + "/deployments", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform({"app_name": app_name}, deployment_list_params.DeploymentListParams), + ), + cast_to=DeploymentListResponse, + ) + async def follow( self, id: str, @@ -371,6 +448,9 @@ def __init__(self, deployments: DeploymentsResource) -> None: self.retrieve = to_raw_response_wrapper( deployments.retrieve, ) + self.list = to_raw_response_wrapper( + deployments.list, + ) self.follow = to_raw_response_wrapper( deployments.follow, ) @@ -386,6 +466,9 @@ def __init__(self, deployments: AsyncDeploymentsResource) -> None: self.retrieve = async_to_raw_response_wrapper( deployments.retrieve, ) + self.list = async_to_raw_response_wrapper( + deployments.list, + ) self.follow = async_to_raw_response_wrapper( deployments.follow, ) @@ -401,6 +484,9 @@ def __init__(self, deployments: DeploymentsResource) -> None: self.retrieve = to_streamed_response_wrapper( deployments.retrieve, ) + self.list = to_streamed_response_wrapper( + deployments.list, + ) self.follow = to_streamed_response_wrapper( deployments.follow, ) @@ -416,6 +502,9 @@ def __init__(self, deployments: AsyncDeploymentsResource) -> None: self.retrieve = async_to_streamed_response_wrapper( deployments.retrieve, ) + self.list = async_to_streamed_response_wrapper( + deployments.list, + ) self.follow = async_to_streamed_response_wrapper( deployments.follow, ) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index c816cf1f..c89d7d5d 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -4,6 +4,7 @@ from .shared import ( LogEvent as LogEvent, + AppAction as AppAction, ErrorEvent as ErrorEvent, ErrorModel as ErrorModel, ErrorDetail as ErrorDetail, @@ -15,11 +16,13 @@ from .browser_create_params import BrowserCreateParams as BrowserCreateParams from .browser_delete_params import BrowserDeleteParams as BrowserDeleteParams from .browser_list_response import BrowserListResponse as BrowserListResponse +from .deployment_list_params import DeploymentListParams as DeploymentListParams from .deployment_state_event import DeploymentStateEvent as DeploymentStateEvent from .invocation_state_event import InvocationStateEvent as InvocationStateEvent from .browser_create_response import BrowserCreateResponse as BrowserCreateResponse from .deployment_create_params import DeploymentCreateParams as DeploymentCreateParams from .deployment_follow_params import DeploymentFollowParams as DeploymentFollowParams +from .deployment_list_response import DeploymentListResponse as DeploymentListResponse from .invocation_create_params import InvocationCreateParams as InvocationCreateParams from .invocation_update_params import InvocationUpdateParams as InvocationUpdateParams from .browser_persistence_param import BrowserPersistenceParam as BrowserPersistenceParam diff --git a/src/kernel/types/app_list_response.py b/src/kernel/types/app_list_response.py index cf8463ea..bdbc3e61 100644 --- a/src/kernel/types/app_list_response.py +++ b/src/kernel/types/app_list_response.py @@ -1,9 +1,10 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Dict, List, Optional +from typing import Dict, List from typing_extensions import Literal, TypeAlias from .._models import BaseModel +from .shared.app_action import AppAction __all__ = ["AppListResponse", "AppListResponseItem"] @@ -12,20 +13,23 @@ class AppListResponseItem(BaseModel): id: str """Unique identifier for the app version""" + actions: List[AppAction] + """List of actions available on the app""" + app_name: str """Name of the application""" deployment: str """Deployment ID""" + env_vars: Dict[str, str] + """Environment variables configured for this app version""" + region: Literal["aws.us-east-1a"] """Deployment region code""" version: str """Version label for the application""" - env_vars: Optional[Dict[str, str]] = None - """Environment variables configured for this app version""" - AppListResponse: TypeAlias = List[AppListResponseItem] diff --git a/src/kernel/types/apps/deployment_create_response.py b/src/kernel/types/apps/deployment_create_response.py index f801195c..8696a0f7 100644 --- a/src/kernel/types/apps/deployment_create_response.py +++ b/src/kernel/types/apps/deployment_create_response.py @@ -4,13 +4,9 @@ from typing_extensions import Literal from ..._models import BaseModel +from ..shared.app_action import AppAction -__all__ = ["DeploymentCreateResponse", "App", "AppAction"] - - -class AppAction(BaseModel): - name: str - """Name of the action""" +__all__ = ["DeploymentCreateResponse", "App"] class App(BaseModel): diff --git a/src/kernel/types/deployment_follow_response.py b/src/kernel/types/deployment_follow_response.py index 00f06856..ca3c512a 100644 --- a/src/kernel/types/deployment_follow_response.py +++ b/src/kernel/types/deployment_follow_response.py @@ -7,23 +7,19 @@ from .._utils import PropertyInfo from .._models import BaseModel from .shared.log_event import LogEvent +from .shared.app_action import AppAction from .shared.error_event import ErrorEvent from .deployment_state_event import DeploymentStateEvent from .shared.heartbeat_event import HeartbeatEvent -__all__ = ["DeploymentFollowResponse", "AppVersionSummaryEvent", "AppVersionSummaryEventAction"] - - -class AppVersionSummaryEventAction(BaseModel): - name: str - """Name of the action""" +__all__ = ["DeploymentFollowResponse", "AppVersionSummaryEvent"] class AppVersionSummaryEvent(BaseModel): id: str """Unique identifier for the app version""" - actions: List[AppVersionSummaryEventAction] + actions: List[AppAction] """List of actions available on the app""" app_name: str diff --git a/src/kernel/types/deployment_list_params.py b/src/kernel/types/deployment_list_params.py new file mode 100644 index 00000000..05704a19 --- /dev/null +++ b/src/kernel/types/deployment_list_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["DeploymentListParams"] + + +class DeploymentListParams(TypedDict, total=False): + app_name: str + """Filter results by application name.""" diff --git a/src/kernel/types/deployment_list_response.py b/src/kernel/types/deployment_list_response.py new file mode 100644 index 00000000..ba7759da --- /dev/null +++ b/src/kernel/types/deployment_list_response.py @@ -0,0 +1,38 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, List, Optional +from datetime import datetime +from typing_extensions import Literal, TypeAlias + +from .._models import BaseModel + +__all__ = ["DeploymentListResponse", "DeploymentListResponseItem"] + + +class DeploymentListResponseItem(BaseModel): + id: str + """Unique identifier for the deployment""" + + created_at: datetime + """Timestamp when the deployment was created""" + + region: Literal["aws.us-east-1a"] + """Deployment region code""" + + status: Literal["queued", "in_progress", "running", "failed", "stopped"] + """Current status of the deployment""" + + entrypoint_rel_path: Optional[str] = None + """Relative path to the application entrypoint""" + + env_vars: Optional[Dict[str, str]] = None + """Environment variables configured for this deployment""" + + status_reason: Optional[str] = None + """Status reason""" + + updated_at: Optional[datetime] = None + """Timestamp when the deployment was last updated""" + + +DeploymentListResponse: TypeAlias = List[DeploymentListResponseItem] diff --git a/src/kernel/types/shared/__init__.py b/src/kernel/types/shared/__init__.py index 60a8de89..ea360f12 100644 --- a/src/kernel/types/shared/__init__.py +++ b/src/kernel/types/shared/__init__.py @@ -1,6 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from .log_event import LogEvent as LogEvent +from .app_action import AppAction as AppAction from .error_event import ErrorEvent as ErrorEvent from .error_model import ErrorModel as ErrorModel from .error_detail import ErrorDetail as ErrorDetail diff --git a/src/kernel/types/shared/app_action.py b/src/kernel/types/shared/app_action.py new file mode 100644 index 00000000..3d711363 --- /dev/null +++ b/src/kernel/types/shared/app_action.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from ..._models import BaseModel + +__all__ = ["AppAction"] + + +class AppAction(BaseModel): + name: str + """Name of the action""" diff --git a/tests/api_resources/test_deployments.py b/tests/api_resources/test_deployments.py index f68c9b04..32214168 100644 --- a/tests/api_resources/test_deployments.py +++ b/tests/api_resources/test_deployments.py @@ -10,6 +10,7 @@ from kernel import Kernel, AsyncKernel from tests.utils import assert_matches_type from kernel.types import ( + DeploymentListResponse, DeploymentCreateResponse, DeploymentRetrieveResponse, ) @@ -112,6 +113,42 @@ def test_path_params_retrieve(self, client: Kernel) -> None: "", ) + @pytest.mark.skip() + @parametrize + def test_method_list(self, client: Kernel) -> None: + deployment = client.deployments.list() + assert_matches_type(DeploymentListResponse, deployment, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_method_list_with_all_params(self, client: Kernel) -> None: + deployment = client.deployments.list( + app_name="app_name", + ) + assert_matches_type(DeploymentListResponse, deployment, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_raw_response_list(self, client: Kernel) -> None: + response = client.deployments.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + deployment = response.parse() + assert_matches_type(DeploymentListResponse, deployment, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_streaming_response_list(self, client: Kernel) -> None: + with client.deployments.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + deployment = response.parse() + assert_matches_type(DeploymentListResponse, deployment, path=["response"]) + + assert cast(Any, response.is_closed) is True + @pytest.mark.skip( reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" ) @@ -270,6 +307,42 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: "", ) + @pytest.mark.skip() + @parametrize + async def test_method_list(self, async_client: AsyncKernel) -> None: + deployment = await async_client.deployments.list() + assert_matches_type(DeploymentListResponse, deployment, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: + deployment = await async_client.deployments.list( + app_name="app_name", + ) + assert_matches_type(DeploymentListResponse, deployment, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_raw_response_list(self, async_client: AsyncKernel) -> None: + response = await async_client.deployments.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + deployment = await response.parse() + assert_matches_type(DeploymentListResponse, deployment, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: + async with async_client.deployments.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + deployment = await response.parse() + assert_matches_type(DeploymentListResponse, deployment, path=["response"]) + + assert cast(Any, response.is_closed) is True + @pytest.mark.skip( reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" ) From 47c2ee110d2ba89dea40ac2997fd22b51bb473c0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 27 Jun 2025 17:02:46 +0000 Subject: [PATCH 106/448] feat(api): manual updates --- .stats.yml | 6 +- api.md | 2 - src/kernel/resources/deployments.py | 91 +------------------ src/kernel/types/__init__.py | 2 - src/kernel/types/app_list_response.py | 12 +-- .../types/apps/deployment_create_response.py | 8 +- src/kernel/types/deployment_list_params.py | 12 --- src/kernel/types/deployment_list_response.py | 38 -------- tests/api_resources/test_deployments.py | 73 --------------- 9 files changed, 14 insertions(+), 230 deletions(-) delete mode 100644 src/kernel/types/deployment_list_params.py delete mode 100644 src/kernel/types/deployment_list_response.py diff --git a/.stats.yml b/.stats.yml index 9625f4ef..731478af 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 17 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2eeb61205775c5997abf8154cd6f6fe81a1e83870eff10050b17ed415aa7860b.yml -openapi_spec_hash: 63405add4a3f53718f8183cbb8c1a22f +configured_endpoints: 16 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-ff8ccba8b5409eaa1128df9027582cb63f66e8accd75e511f70b7c27ef26c9ae.yml +openapi_spec_hash: 1dbacc339695a7c78718f90f791d3f01 config_hash: 00ec9df250b9dc077f8d3b93a442d252 diff --git a/api.md b/api.md index 49538b67..ab8cb4fe 100644 --- a/api.md +++ b/api.md @@ -13,7 +13,6 @@ from kernel.types import ( DeploymentStateEvent, DeploymentCreateResponse, DeploymentRetrieveResponse, - DeploymentListResponse, DeploymentFollowResponse, ) ``` @@ -22,7 +21,6 @@ Methods: - client.deployments.create(\*\*params) -> DeploymentCreateResponse - client.deployments.retrieve(id) -> DeploymentRetrieveResponse -- client.deployments.list(\*\*params) -> DeploymentListResponse - client.deployments.follow(id, \*\*params) -> DeploymentFollowResponse # Apps diff --git a/src/kernel/resources/deployments.py b/src/kernel/resources/deployments.py index d54c4ec2..f27c894d 100644 --- a/src/kernel/resources/deployments.py +++ b/src/kernel/resources/deployments.py @@ -7,7 +7,7 @@ import httpx -from ..types import deployment_list_params, deployment_create_params, deployment_follow_params +from ..types import deployment_create_params, deployment_follow_params from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform from .._compat import cached_property @@ -20,7 +20,6 @@ ) from .._streaming import Stream, AsyncStream from .._base_client import make_request_options -from ..types.deployment_list_response import DeploymentListResponse from ..types.deployment_create_response import DeploymentCreateResponse from ..types.deployment_follow_response import DeploymentFollowResponse from ..types.deployment_retrieve_response import DeploymentRetrieveResponse @@ -147,44 +146,6 @@ def retrieve( cast_to=DeploymentRetrieveResponse, ) - def list( - self, - *, - app_name: 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, - ) -> DeploymentListResponse: - """List deployments. - - Optionally filter by application name. - - Args: - app_name: Filter results by application name. - - 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( - "/deployments", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=maybe_transform({"app_name": app_name}, deployment_list_params.DeploymentListParams), - ), - cast_to=DeploymentListResponse, - ) - def follow( self, id: str, @@ -352,44 +313,6 @@ async def retrieve( cast_to=DeploymentRetrieveResponse, ) - async def list( - self, - *, - app_name: 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, - ) -> DeploymentListResponse: - """List deployments. - - Optionally filter by application name. - - Args: - app_name: Filter results by application name. - - 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( - "/deployments", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=await async_maybe_transform({"app_name": app_name}, deployment_list_params.DeploymentListParams), - ), - cast_to=DeploymentListResponse, - ) - async def follow( self, id: str, @@ -448,9 +371,6 @@ def __init__(self, deployments: DeploymentsResource) -> None: self.retrieve = to_raw_response_wrapper( deployments.retrieve, ) - self.list = to_raw_response_wrapper( - deployments.list, - ) self.follow = to_raw_response_wrapper( deployments.follow, ) @@ -466,9 +386,6 @@ def __init__(self, deployments: AsyncDeploymentsResource) -> None: self.retrieve = async_to_raw_response_wrapper( deployments.retrieve, ) - self.list = async_to_raw_response_wrapper( - deployments.list, - ) self.follow = async_to_raw_response_wrapper( deployments.follow, ) @@ -484,9 +401,6 @@ def __init__(self, deployments: DeploymentsResource) -> None: self.retrieve = to_streamed_response_wrapper( deployments.retrieve, ) - self.list = to_streamed_response_wrapper( - deployments.list, - ) self.follow = to_streamed_response_wrapper( deployments.follow, ) @@ -502,9 +416,6 @@ def __init__(self, deployments: AsyncDeploymentsResource) -> None: self.retrieve = async_to_streamed_response_wrapper( deployments.retrieve, ) - self.list = async_to_streamed_response_wrapper( - deployments.list, - ) self.follow = async_to_streamed_response_wrapper( deployments.follow, ) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index c89d7d5d..a0b086d9 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -16,13 +16,11 @@ from .browser_create_params import BrowserCreateParams as BrowserCreateParams from .browser_delete_params import BrowserDeleteParams as BrowserDeleteParams from .browser_list_response import BrowserListResponse as BrowserListResponse -from .deployment_list_params import DeploymentListParams as DeploymentListParams from .deployment_state_event import DeploymentStateEvent as DeploymentStateEvent from .invocation_state_event import InvocationStateEvent as InvocationStateEvent from .browser_create_response import BrowserCreateResponse as BrowserCreateResponse from .deployment_create_params import DeploymentCreateParams as DeploymentCreateParams from .deployment_follow_params import DeploymentFollowParams as DeploymentFollowParams -from .deployment_list_response import DeploymentListResponse as DeploymentListResponse from .invocation_create_params import InvocationCreateParams as InvocationCreateParams from .invocation_update_params import InvocationUpdateParams as InvocationUpdateParams from .browser_persistence_param import BrowserPersistenceParam as BrowserPersistenceParam diff --git a/src/kernel/types/app_list_response.py b/src/kernel/types/app_list_response.py index bdbc3e61..cf8463ea 100644 --- a/src/kernel/types/app_list_response.py +++ b/src/kernel/types/app_list_response.py @@ -1,10 +1,9 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Dict, List +from typing import Dict, List, Optional from typing_extensions import Literal, TypeAlias from .._models import BaseModel -from .shared.app_action import AppAction __all__ = ["AppListResponse", "AppListResponseItem"] @@ -13,23 +12,20 @@ class AppListResponseItem(BaseModel): id: str """Unique identifier for the app version""" - actions: List[AppAction] - """List of actions available on the app""" - app_name: str """Name of the application""" deployment: str """Deployment ID""" - env_vars: Dict[str, str] - """Environment variables configured for this app version""" - region: Literal["aws.us-east-1a"] """Deployment region code""" version: str """Version label for the application""" + env_vars: Optional[Dict[str, str]] = None + """Environment variables configured for this app version""" + AppListResponse: TypeAlias = List[AppListResponseItem] diff --git a/src/kernel/types/apps/deployment_create_response.py b/src/kernel/types/apps/deployment_create_response.py index 8696a0f7..f801195c 100644 --- a/src/kernel/types/apps/deployment_create_response.py +++ b/src/kernel/types/apps/deployment_create_response.py @@ -4,9 +4,13 @@ from typing_extensions import Literal from ..._models import BaseModel -from ..shared.app_action import AppAction -__all__ = ["DeploymentCreateResponse", "App"] +__all__ = ["DeploymentCreateResponse", "App", "AppAction"] + + +class AppAction(BaseModel): + name: str + """Name of the action""" class App(BaseModel): diff --git a/src/kernel/types/deployment_list_params.py b/src/kernel/types/deployment_list_params.py deleted file mode 100644 index 05704a19..00000000 --- a/src/kernel/types/deployment_list_params.py +++ /dev/null @@ -1,12 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import TypedDict - -__all__ = ["DeploymentListParams"] - - -class DeploymentListParams(TypedDict, total=False): - app_name: str - """Filter results by application name.""" diff --git a/src/kernel/types/deployment_list_response.py b/src/kernel/types/deployment_list_response.py deleted file mode 100644 index ba7759da..00000000 --- a/src/kernel/types/deployment_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 Dict, List, Optional -from datetime import datetime -from typing_extensions import Literal, TypeAlias - -from .._models import BaseModel - -__all__ = ["DeploymentListResponse", "DeploymentListResponseItem"] - - -class DeploymentListResponseItem(BaseModel): - id: str - """Unique identifier for the deployment""" - - created_at: datetime - """Timestamp when the deployment was created""" - - region: Literal["aws.us-east-1a"] - """Deployment region code""" - - status: Literal["queued", "in_progress", "running", "failed", "stopped"] - """Current status of the deployment""" - - entrypoint_rel_path: Optional[str] = None - """Relative path to the application entrypoint""" - - env_vars: Optional[Dict[str, str]] = None - """Environment variables configured for this deployment""" - - status_reason: Optional[str] = None - """Status reason""" - - updated_at: Optional[datetime] = None - """Timestamp when the deployment was last updated""" - - -DeploymentListResponse: TypeAlias = List[DeploymentListResponseItem] diff --git a/tests/api_resources/test_deployments.py b/tests/api_resources/test_deployments.py index 32214168..f68c9b04 100644 --- a/tests/api_resources/test_deployments.py +++ b/tests/api_resources/test_deployments.py @@ -10,7 +10,6 @@ from kernel import Kernel, AsyncKernel from tests.utils import assert_matches_type from kernel.types import ( - DeploymentListResponse, DeploymentCreateResponse, DeploymentRetrieveResponse, ) @@ -113,42 +112,6 @@ def test_path_params_retrieve(self, client: Kernel) -> None: "", ) - @pytest.mark.skip() - @parametrize - def test_method_list(self, client: Kernel) -> None: - deployment = client.deployments.list() - assert_matches_type(DeploymentListResponse, deployment, path=["response"]) - - @pytest.mark.skip() - @parametrize - def test_method_list_with_all_params(self, client: Kernel) -> None: - deployment = client.deployments.list( - app_name="app_name", - ) - assert_matches_type(DeploymentListResponse, deployment, path=["response"]) - - @pytest.mark.skip() - @parametrize - def test_raw_response_list(self, client: Kernel) -> None: - response = client.deployments.with_raw_response.list() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - deployment = response.parse() - assert_matches_type(DeploymentListResponse, deployment, path=["response"]) - - @pytest.mark.skip() - @parametrize - def test_streaming_response_list(self, client: Kernel) -> None: - with client.deployments.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - deployment = response.parse() - assert_matches_type(DeploymentListResponse, deployment, path=["response"]) - - assert cast(Any, response.is_closed) is True - @pytest.mark.skip( reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" ) @@ -307,42 +270,6 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: "", ) - @pytest.mark.skip() - @parametrize - async def test_method_list(self, async_client: AsyncKernel) -> None: - deployment = await async_client.deployments.list() - assert_matches_type(DeploymentListResponse, deployment, path=["response"]) - - @pytest.mark.skip() - @parametrize - async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: - deployment = await async_client.deployments.list( - app_name="app_name", - ) - assert_matches_type(DeploymentListResponse, deployment, path=["response"]) - - @pytest.mark.skip() - @parametrize - async def test_raw_response_list(self, async_client: AsyncKernel) -> None: - response = await async_client.deployments.with_raw_response.list() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - deployment = await response.parse() - assert_matches_type(DeploymentListResponse, deployment, path=["response"]) - - @pytest.mark.skip() - @parametrize - async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: - async with async_client.deployments.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - deployment = await response.parse() - assert_matches_type(DeploymentListResponse, deployment, path=["response"]) - - assert cast(Any, response.is_closed) is True - @pytest.mark.skip( reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" ) From 4a822fb205b995691c207cbe5e7ce35985616650 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 27 Jun 2025 17:04:01 +0000 Subject: [PATCH 107/448] feat(api): deployments --- .stats.yml | 6 +- api.md | 2 + src/kernel/resources/deployments.py | 91 ++++++++++++++++++- src/kernel/types/__init__.py | 2 + src/kernel/types/app_list_response.py | 12 ++- .../types/apps/deployment_create_response.py | 8 +- src/kernel/types/deployment_list_params.py | 12 +++ src/kernel/types/deployment_list_response.py | 38 ++++++++ tests/api_resources/test_deployments.py | 73 +++++++++++++++ 9 files changed, 230 insertions(+), 14 deletions(-) create mode 100644 src/kernel/types/deployment_list_params.py create mode 100644 src/kernel/types/deployment_list_response.py diff --git a/.stats.yml b/.stats.yml index 731478af..9625f4ef 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 16 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-ff8ccba8b5409eaa1128df9027582cb63f66e8accd75e511f70b7c27ef26c9ae.yml -openapi_spec_hash: 1dbacc339695a7c78718f90f791d3f01 +configured_endpoints: 17 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2eeb61205775c5997abf8154cd6f6fe81a1e83870eff10050b17ed415aa7860b.yml +openapi_spec_hash: 63405add4a3f53718f8183cbb8c1a22f config_hash: 00ec9df250b9dc077f8d3b93a442d252 diff --git a/api.md b/api.md index ab8cb4fe..49538b67 100644 --- a/api.md +++ b/api.md @@ -13,6 +13,7 @@ from kernel.types import ( DeploymentStateEvent, DeploymentCreateResponse, DeploymentRetrieveResponse, + DeploymentListResponse, DeploymentFollowResponse, ) ``` @@ -21,6 +22,7 @@ Methods: - client.deployments.create(\*\*params) -> DeploymentCreateResponse - client.deployments.retrieve(id) -> DeploymentRetrieveResponse +- client.deployments.list(\*\*params) -> DeploymentListResponse - client.deployments.follow(id, \*\*params) -> DeploymentFollowResponse # Apps diff --git a/src/kernel/resources/deployments.py b/src/kernel/resources/deployments.py index f27c894d..d54c4ec2 100644 --- a/src/kernel/resources/deployments.py +++ b/src/kernel/resources/deployments.py @@ -7,7 +7,7 @@ import httpx -from ..types import deployment_create_params, deployment_follow_params +from ..types import deployment_list_params, deployment_create_params, deployment_follow_params from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform from .._compat import cached_property @@ -20,6 +20,7 @@ ) from .._streaming import Stream, AsyncStream from .._base_client import make_request_options +from ..types.deployment_list_response import DeploymentListResponse from ..types.deployment_create_response import DeploymentCreateResponse from ..types.deployment_follow_response import DeploymentFollowResponse from ..types.deployment_retrieve_response import DeploymentRetrieveResponse @@ -146,6 +147,44 @@ def retrieve( cast_to=DeploymentRetrieveResponse, ) + def list( + self, + *, + app_name: 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, + ) -> DeploymentListResponse: + """List deployments. + + Optionally filter by application name. + + Args: + app_name: Filter results by application name. + + 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( + "/deployments", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform({"app_name": app_name}, deployment_list_params.DeploymentListParams), + ), + cast_to=DeploymentListResponse, + ) + def follow( self, id: str, @@ -313,6 +352,44 @@ async def retrieve( cast_to=DeploymentRetrieveResponse, ) + async def list( + self, + *, + app_name: 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, + ) -> DeploymentListResponse: + """List deployments. + + Optionally filter by application name. + + Args: + app_name: Filter results by application name. + + 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( + "/deployments", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform({"app_name": app_name}, deployment_list_params.DeploymentListParams), + ), + cast_to=DeploymentListResponse, + ) + async def follow( self, id: str, @@ -371,6 +448,9 @@ def __init__(self, deployments: DeploymentsResource) -> None: self.retrieve = to_raw_response_wrapper( deployments.retrieve, ) + self.list = to_raw_response_wrapper( + deployments.list, + ) self.follow = to_raw_response_wrapper( deployments.follow, ) @@ -386,6 +466,9 @@ def __init__(self, deployments: AsyncDeploymentsResource) -> None: self.retrieve = async_to_raw_response_wrapper( deployments.retrieve, ) + self.list = async_to_raw_response_wrapper( + deployments.list, + ) self.follow = async_to_raw_response_wrapper( deployments.follow, ) @@ -401,6 +484,9 @@ def __init__(self, deployments: DeploymentsResource) -> None: self.retrieve = to_streamed_response_wrapper( deployments.retrieve, ) + self.list = to_streamed_response_wrapper( + deployments.list, + ) self.follow = to_streamed_response_wrapper( deployments.follow, ) @@ -416,6 +502,9 @@ def __init__(self, deployments: AsyncDeploymentsResource) -> None: self.retrieve = async_to_streamed_response_wrapper( deployments.retrieve, ) + self.list = async_to_streamed_response_wrapper( + deployments.list, + ) self.follow = async_to_streamed_response_wrapper( deployments.follow, ) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index a0b086d9..c89d7d5d 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -16,11 +16,13 @@ from .browser_create_params import BrowserCreateParams as BrowserCreateParams from .browser_delete_params import BrowserDeleteParams as BrowserDeleteParams from .browser_list_response import BrowserListResponse as BrowserListResponse +from .deployment_list_params import DeploymentListParams as DeploymentListParams from .deployment_state_event import DeploymentStateEvent as DeploymentStateEvent from .invocation_state_event import InvocationStateEvent as InvocationStateEvent from .browser_create_response import BrowserCreateResponse as BrowserCreateResponse from .deployment_create_params import DeploymentCreateParams as DeploymentCreateParams from .deployment_follow_params import DeploymentFollowParams as DeploymentFollowParams +from .deployment_list_response import DeploymentListResponse as DeploymentListResponse from .invocation_create_params import InvocationCreateParams as InvocationCreateParams from .invocation_update_params import InvocationUpdateParams as InvocationUpdateParams from .browser_persistence_param import BrowserPersistenceParam as BrowserPersistenceParam diff --git a/src/kernel/types/app_list_response.py b/src/kernel/types/app_list_response.py index cf8463ea..bdbc3e61 100644 --- a/src/kernel/types/app_list_response.py +++ b/src/kernel/types/app_list_response.py @@ -1,9 +1,10 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Dict, List, Optional +from typing import Dict, List from typing_extensions import Literal, TypeAlias from .._models import BaseModel +from .shared.app_action import AppAction __all__ = ["AppListResponse", "AppListResponseItem"] @@ -12,20 +13,23 @@ class AppListResponseItem(BaseModel): id: str """Unique identifier for the app version""" + actions: List[AppAction] + """List of actions available on the app""" + app_name: str """Name of the application""" deployment: str """Deployment ID""" + env_vars: Dict[str, str] + """Environment variables configured for this app version""" + region: Literal["aws.us-east-1a"] """Deployment region code""" version: str """Version label for the application""" - env_vars: Optional[Dict[str, str]] = None - """Environment variables configured for this app version""" - AppListResponse: TypeAlias = List[AppListResponseItem] diff --git a/src/kernel/types/apps/deployment_create_response.py b/src/kernel/types/apps/deployment_create_response.py index f801195c..8696a0f7 100644 --- a/src/kernel/types/apps/deployment_create_response.py +++ b/src/kernel/types/apps/deployment_create_response.py @@ -4,13 +4,9 @@ from typing_extensions import Literal from ..._models import BaseModel +from ..shared.app_action import AppAction -__all__ = ["DeploymentCreateResponse", "App", "AppAction"] - - -class AppAction(BaseModel): - name: str - """Name of the action""" +__all__ = ["DeploymentCreateResponse", "App"] class App(BaseModel): diff --git a/src/kernel/types/deployment_list_params.py b/src/kernel/types/deployment_list_params.py new file mode 100644 index 00000000..05704a19 --- /dev/null +++ b/src/kernel/types/deployment_list_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["DeploymentListParams"] + + +class DeploymentListParams(TypedDict, total=False): + app_name: str + """Filter results by application name.""" diff --git a/src/kernel/types/deployment_list_response.py b/src/kernel/types/deployment_list_response.py new file mode 100644 index 00000000..ba7759da --- /dev/null +++ b/src/kernel/types/deployment_list_response.py @@ -0,0 +1,38 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, List, Optional +from datetime import datetime +from typing_extensions import Literal, TypeAlias + +from .._models import BaseModel + +__all__ = ["DeploymentListResponse", "DeploymentListResponseItem"] + + +class DeploymentListResponseItem(BaseModel): + id: str + """Unique identifier for the deployment""" + + created_at: datetime + """Timestamp when the deployment was created""" + + region: Literal["aws.us-east-1a"] + """Deployment region code""" + + status: Literal["queued", "in_progress", "running", "failed", "stopped"] + """Current status of the deployment""" + + entrypoint_rel_path: Optional[str] = None + """Relative path to the application entrypoint""" + + env_vars: Optional[Dict[str, str]] = None + """Environment variables configured for this deployment""" + + status_reason: Optional[str] = None + """Status reason""" + + updated_at: Optional[datetime] = None + """Timestamp when the deployment was last updated""" + + +DeploymentListResponse: TypeAlias = List[DeploymentListResponseItem] diff --git a/tests/api_resources/test_deployments.py b/tests/api_resources/test_deployments.py index f68c9b04..32214168 100644 --- a/tests/api_resources/test_deployments.py +++ b/tests/api_resources/test_deployments.py @@ -10,6 +10,7 @@ from kernel import Kernel, AsyncKernel from tests.utils import assert_matches_type from kernel.types import ( + DeploymentListResponse, DeploymentCreateResponse, DeploymentRetrieveResponse, ) @@ -112,6 +113,42 @@ def test_path_params_retrieve(self, client: Kernel) -> None: "", ) + @pytest.mark.skip() + @parametrize + def test_method_list(self, client: Kernel) -> None: + deployment = client.deployments.list() + assert_matches_type(DeploymentListResponse, deployment, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_method_list_with_all_params(self, client: Kernel) -> None: + deployment = client.deployments.list( + app_name="app_name", + ) + assert_matches_type(DeploymentListResponse, deployment, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_raw_response_list(self, client: Kernel) -> None: + response = client.deployments.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + deployment = response.parse() + assert_matches_type(DeploymentListResponse, deployment, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_streaming_response_list(self, client: Kernel) -> None: + with client.deployments.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + deployment = response.parse() + assert_matches_type(DeploymentListResponse, deployment, path=["response"]) + + assert cast(Any, response.is_closed) is True + @pytest.mark.skip( reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" ) @@ -270,6 +307,42 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: "", ) + @pytest.mark.skip() + @parametrize + async def test_method_list(self, async_client: AsyncKernel) -> None: + deployment = await async_client.deployments.list() + assert_matches_type(DeploymentListResponse, deployment, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: + deployment = await async_client.deployments.list( + app_name="app_name", + ) + assert_matches_type(DeploymentListResponse, deployment, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_raw_response_list(self, async_client: AsyncKernel) -> None: + response = await async_client.deployments.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + deployment = await response.parse() + assert_matches_type(DeploymentListResponse, deployment, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: + async with async_client.deployments.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + deployment = await response.parse() + assert_matches_type(DeploymentListResponse, deployment, path=["response"]) + + assert cast(Any, response.is_closed) is True + @pytest.mark.skip( reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" ) From 2a3b1ac005e56c77418d27d267ef19f304454677 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 28 Jun 2025 03:14:42 +0000 Subject: [PATCH 108/448] chore(internal): codegen related update --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 5c87ad82..12aa8969 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.6.3" + ".": "0.6.4" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index cec894e7..0c6e6abf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.6.3" +version = "0.6.4" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 8903bb23..e0e69ec6 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.6.3" # x-release-please-version +__version__ = "0.6.4" # x-release-please-version From e77d5f4a80251e8265ed8dc61afa02076f5a9bea Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 28 Jun 2025 08:45:32 +0000 Subject: [PATCH 109/448] chore(ci): only run for pushes and fork pull requests --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3f5bc46..596c3719 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,7 @@ jobs: timeout-minutes: 10 name: lint runs-on: ${{ github.repository == 'stainless-sdks/kernel-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@v4 @@ -42,6 +43,7 @@ jobs: contents: read id-token: write runs-on: depot-ubuntu-24.04 + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@v4 @@ -62,6 +64,7 @@ jobs: timeout-minutes: 10 name: test runs-on: ${{ github.repository == 'stainless-sdks/kernel-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@v4 From 713edb96bd6ec8a7f0c62f58ae28594bc807a3fd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 02:29:51 +0000 Subject: [PATCH 110/448] fix(ci): correct conditional --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 596c3719..d53ac62a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,14 +36,13 @@ jobs: run: ./scripts/lint upload: - if: github.repository == 'stainless-sdks/kernel-python' + if: github.repository == 'stainless-sdks/kernel-python' && (github.event_name == 'push' || github.event.pull_request.head.repo.fork) timeout-minutes: 10 name: upload permissions: contents: read id-token: write runs-on: depot-ubuntu-24.04 - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@v4 From bd31be2e0d420fb78805b5cebe1582d102b93cc4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 05:04:51 +0000 Subject: [PATCH 111/448] chore(ci): change upload type --- .github/workflows/ci.yml | 18 ++++++++++++++++-- scripts/utils/upload-artifact.sh | 12 +++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d53ac62a..1692944e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,10 +35,10 @@ jobs: - name: Run lints run: ./scripts/lint - upload: + build: if: github.repository == 'stainless-sdks/kernel-python' && (github.event_name == 'push' || github.event.pull_request.head.repo.fork) timeout-minutes: 10 - name: upload + name: build permissions: contents: read id-token: write @@ -46,6 +46,20 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Install Rye + run: | + curl -sSf https://rye.astral.sh/get | bash + echo "$HOME/.rye/shims" >> $GITHUB_PATH + env: + RYE_VERSION: '0.44.0' + RYE_INSTALL_OPTION: '--yes' + + - name: Install dependencies + run: rye sync --all-features + + - name: Run build + run: rye build + - name: Get GitHub OIDC Token id: github-oidc uses: actions/github-script@v6 diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh index 7b344b4f..14b2cc82 100755 --- a/scripts/utils/upload-artifact.sh +++ b/scripts/utils/upload-artifact.sh @@ -1,7 +1,9 @@ #!/usr/bin/env bash set -exuo pipefail -RESPONSE=$(curl -X POST "$URL" \ +FILENAME=$(basename dist/*.whl) + +RESPONSE=$(curl -X POST "$URL?filename=$FILENAME" \ -H "Authorization: Bearer $AUTH" \ -H "Content-Type: application/json") @@ -12,13 +14,13 @@ if [[ "$SIGNED_URL" == "null" ]]; then exit 1 fi -UPLOAD_RESPONSE=$(tar -cz . | curl -v -X PUT \ - -H "Content-Type: application/gzip" \ - --data-binary @- "$SIGNED_URL" 2>&1) +UPLOAD_RESPONSE=$(curl -v -X PUT \ + -H "Content-Type: binary/octet-stream" \ + --data-binary "@dist/$FILENAME" "$SIGNED_URL" 2>&1) if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then echo -e "\033[32mUploaded build to Stainless storage.\033[0m" - echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/kernel-python/$SHA'\033[0m" + echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/kernel-python/$SHA/$FILENAME'\033[0m" else echo -e "\033[31mFailed to upload artifact.\033[0m" exit 1 From 6004fd55ab456dcd1b1b613a2af370811cd7872b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 19:04:18 +0000 Subject: [PATCH 112/448] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 9625f4ef..d45a618a 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 17 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2eeb61205775c5997abf8154cd6f6fe81a1e83870eff10050b17ed415aa7860b.yml -openapi_spec_hash: 63405add4a3f53718f8183cbb8c1a22f +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-0ac9428eb663361184124cdd6a6e80ae8dc72c927626c949f22aacc4f40095de.yml +openapi_spec_hash: 27707667d706ac33f2d9ccb23c0f15c3 config_hash: 00ec9df250b9dc077f8d3b93a442d252 From f294940dbb75a6b43bf900ee57985a6ef95d2dc1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 21:49:30 +0000 Subject: [PATCH 113/448] feat(api): headless browsers --- .stats.yml | 4 ++-- src/kernel/resources/browsers.py | 10 ++++++++++ src/kernel/types/browser_create_params.py | 6 ++++++ src/kernel/types/browser_create_response.py | 9 ++++++--- src/kernel/types/browser_list_response.py | 9 ++++++--- src/kernel/types/browser_retrieve_response.py | 9 ++++++--- tests/api_resources/test_browsers.py | 2 ++ 7 files changed, 38 insertions(+), 11 deletions(-) diff --git a/.stats.yml b/.stats.yml index d45a618a..401ed656 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 17 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-0ac9428eb663361184124cdd6a6e80ae8dc72c927626c949f22aacc4f40095de.yml -openapi_spec_hash: 27707667d706ac33f2d9ccb23c0f15c3 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-3ec96d0022acb32aa2676c2e7ae20152b899a776ccd499380c334c955b9ba071.yml +openapi_spec_hash: b64c095d82185c1cd0355abea88b606f config_hash: 00ec9df250b9dc077f8d3b93a442d252 diff --git a/src/kernel/resources/browsers.py b/src/kernel/resources/browsers.py index 0cbe8201..56a95f07 100644 --- a/src/kernel/resources/browsers.py +++ b/src/kernel/resources/browsers.py @@ -47,6 +47,7 @@ def with_streaming_response(self) -> BrowsersResourceWithStreamingResponse: def create( self, *, + headless: bool | NotGiven = NOT_GIVEN, invocation_id: str | NotGiven = NOT_GIVEN, persistence: BrowserPersistenceParam | NotGiven = NOT_GIVEN, stealth: bool | NotGiven = NOT_GIVEN, @@ -61,6 +62,9 @@ def create( Create a new browser session from within an action. Args: + headless: If true, launches the browser using a headless image (no VNC/GUI). Defaults to + false. + invocation_id: action invocation ID persistence: Optional persistence configuration for the browser session. @@ -80,6 +84,7 @@ def create( "/browsers", body=maybe_transform( { + "headless": headless, "invocation_id": invocation_id, "persistence": persistence, "stealth": stealth, @@ -240,6 +245,7 @@ def with_streaming_response(self) -> AsyncBrowsersResourceWithStreamingResponse: async def create( self, *, + headless: bool | NotGiven = NOT_GIVEN, invocation_id: str | NotGiven = NOT_GIVEN, persistence: BrowserPersistenceParam | NotGiven = NOT_GIVEN, stealth: bool | NotGiven = NOT_GIVEN, @@ -254,6 +260,9 @@ async def create( Create a new browser session from within an action. Args: + headless: If true, launches the browser using a headless image (no VNC/GUI). Defaults to + false. + invocation_id: action invocation ID persistence: Optional persistence configuration for the browser session. @@ -273,6 +282,7 @@ async def create( "/browsers", body=await async_maybe_transform( { + "headless": headless, "invocation_id": invocation_id, "persistence": persistence, "stealth": stealth, diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index 2153abf9..746a92f9 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -10,6 +10,12 @@ class BrowserCreateParams(TypedDict, total=False): + headless: bool + """If true, launches the browser using a headless image (no VNC/GUI). + + Defaults to false. + """ + invocation_id: str """action invocation ID""" diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index f44f3360..afba2b32 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -9,14 +9,17 @@ class BrowserCreateResponse(BaseModel): - browser_live_view_url: str - """Remote URL for live viewing the browser session""" - cdp_ws_url: str """Websocket URL for Chrome DevTools Protocol connections to the browser session""" session_id: str """Unique identifier for the browser session""" + browser_live_view_url: Optional[str] = None + """Remote URL for live viewing the browser session. + + Only available for non-headless browsers. + """ + persistence: Optional[BrowserPersistence] = None """Optional persistence configuration for the browser session.""" diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index d3e90d53..43c8d924 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -10,15 +10,18 @@ class BrowserListResponseItem(BaseModel): - browser_live_view_url: str - """Remote URL for live viewing the browser session""" - cdp_ws_url: str """Websocket URL for Chrome DevTools Protocol connections to the browser session""" session_id: str """Unique identifier for the browser session""" + browser_live_view_url: Optional[str] = None + """Remote URL for live viewing the browser session. + + Only available for non-headless browsers. + """ + persistence: Optional[BrowserPersistence] = None """Optional persistence configuration for the browser session.""" diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 8676b532..45cf74b1 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -9,14 +9,17 @@ class BrowserRetrieveResponse(BaseModel): - browser_live_view_url: str - """Remote URL for live viewing the browser session""" - cdp_ws_url: str """Websocket URL for Chrome DevTools Protocol connections to the browser session""" session_id: str """Unique identifier for the browser session""" + browser_live_view_url: Optional[str] = None + """Remote URL for live viewing the browser session. + + Only available for non-headless browsers. + """ + persistence: Optional[BrowserPersistence] = None """Optional persistence configuration for the browser session.""" diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index f4111fa6..8f990bea 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -31,6 +31,7 @@ def test_method_create(self, client: Kernel) -> None: @parametrize def test_method_create_with_all_params(self, client: Kernel) -> None: browser = client.browsers.create( + headless=False, invocation_id="rr33xuugxj9h0bkf1rdt2bet", persistence={"id": "my-awesome-browser-for-user-1234"}, stealth=True, @@ -221,6 +222,7 @@ async def test_method_create(self, async_client: AsyncKernel) -> None: @parametrize async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: browser = await async_client.browsers.create( + headless=False, invocation_id="rr33xuugxj9h0bkf1rdt2bet", persistence={"id": "my-awesome-browser-for-user-1234"}, stealth=True, From 8a902de414d5d1f5cac150c9236b419b9ac07fea Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 8 Jul 2025 02:13:13 +0000 Subject: [PATCH 114/448] chore(internal): codegen related update --- requirements-dev.lock | 2 +- requirements.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-dev.lock b/requirements-dev.lock index 27de013f..f4090db6 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -56,7 +56,7 @@ httpx==0.28.1 # via httpx-aiohttp # via kernel # via respx -httpx-aiohttp==0.1.6 +httpx-aiohttp==0.1.8 # via kernel idna==3.4 # via anyio diff --git a/requirements.lock b/requirements.lock index 4006aa29..c125a9e6 100644 --- a/requirements.lock +++ b/requirements.lock @@ -43,7 +43,7 @@ httpcore==1.0.2 httpx==0.28.1 # via httpx-aiohttp # via kernel -httpx-aiohttp==0.1.6 +httpx-aiohttp==0.1.8 # via kernel idna==3.4 # via anyio From 2c2c0f22ce47753b9b3f53a9b6edca58dffeca19 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 8 Jul 2025 18:15:22 +0000 Subject: [PATCH 115/448] feat(api): manual updates --- .stats.yml | 8 +- api.md | 1 + src/kernel/resources/browsers.py | 100 ++++++++++++++ src/kernel/types/browser_create_params.py | 3 + src/kernel/types/browser_create_response.py | 3 + src/kernel/types/browser_list_response.py | 3 + src/kernel/types/browser_retrieve_response.py | 3 + tests/api_resources/test_browsers.py | 130 ++++++++++++++++++ 8 files changed, 247 insertions(+), 4 deletions(-) diff --git a/.stats.yml b/.stats.yml index 401ed656..aa1aff73 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 17 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-3ec96d0022acb32aa2676c2e7ae20152b899a776ccd499380c334c955b9ba071.yml -openapi_spec_hash: b64c095d82185c1cd0355abea88b606f -config_hash: 00ec9df250b9dc077f8d3b93a442d252 +configured_endpoints: 18 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-d173129101e26f450c200e84430d993479c034700cf826917425d513b88912e6.yml +openapi_spec_hash: 150b86da7588979d7619b1a894e4720c +config_hash: eaeed470b1070b34df69c49d68e67355 diff --git a/api.md b/api.md index 49538b67..8a0fbd3e 100644 --- a/api.md +++ b/api.md @@ -92,3 +92,4 @@ Methods: - client.browsers.list() -> BrowserListResponse - client.browsers.delete(\*\*params) -> None - client.browsers.delete_by_id(id) -> None +- client.browsers.retrieve_replay(id) -> BinaryAPIResponse diff --git a/src/kernel/resources/browsers.py b/src/kernel/resources/browsers.py index 56a95f07..01b529bb 100644 --- a/src/kernel/resources/browsers.py +++ b/src/kernel/resources/browsers.py @@ -10,10 +10,18 @@ from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, to_raw_response_wrapper, to_streamed_response_wrapper, async_to_raw_response_wrapper, + to_custom_raw_response_wrapper, async_to_streamed_response_wrapper, + to_custom_streamed_response_wrapper, + async_to_custom_raw_response_wrapper, + async_to_custom_streamed_response_wrapper, ) from .._base_client import make_request_options from ..types.browser_list_response import BrowserListResponse @@ -50,6 +58,7 @@ def create( headless: bool | NotGiven = NOT_GIVEN, invocation_id: str | NotGiven = NOT_GIVEN, persistence: BrowserPersistenceParam | NotGiven = NOT_GIVEN, + replay: bool | NotGiven = NOT_GIVEN, stealth: 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. @@ -69,6 +78,8 @@ def create( persistence: Optional persistence configuration for the browser session. + replay: If true, enables replay recording of the browser session. Defaults to false. + stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. @@ -87,6 +98,7 @@ def create( "headless": headless, "invocation_id": invocation_id, "persistence": persistence, + "replay": replay, "stealth": stealth, }, browser_create_params.BrowserCreateParams, @@ -221,6 +233,40 @@ def delete_by_id( cast_to=NoneType, ) + def retrieve_replay( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BinaryAPIResponse: + """ + Get browser session replay. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "video/mp4", **(extra_headers or {})} + return self._get( + f"/browsers/{id}/replay", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BinaryAPIResponse, + ) + class AsyncBrowsersResource(AsyncAPIResource): @cached_property @@ -248,6 +294,7 @@ async def create( headless: bool | NotGiven = NOT_GIVEN, invocation_id: str | NotGiven = NOT_GIVEN, persistence: BrowserPersistenceParam | NotGiven = NOT_GIVEN, + replay: bool | NotGiven = NOT_GIVEN, stealth: 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. @@ -267,6 +314,8 @@ async def create( persistence: Optional persistence configuration for the browser session. + replay: If true, enables replay recording of the browser session. Defaults to false. + stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. @@ -285,6 +334,7 @@ async def create( "headless": headless, "invocation_id": invocation_id, "persistence": persistence, + "replay": replay, "stealth": stealth, }, browser_create_params.BrowserCreateParams, @@ -421,6 +471,40 @@ async def delete_by_id( cast_to=NoneType, ) + async def retrieve_replay( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncBinaryAPIResponse: + """ + Get browser session replay. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "video/mp4", **(extra_headers or {})} + return await self._get( + f"/browsers/{id}/replay", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AsyncBinaryAPIResponse, + ) + class BrowsersResourceWithRawResponse: def __init__(self, browsers: BrowsersResource) -> None: @@ -441,6 +525,10 @@ def __init__(self, browsers: BrowsersResource) -> None: self.delete_by_id = to_raw_response_wrapper( browsers.delete_by_id, ) + self.retrieve_replay = to_custom_raw_response_wrapper( + browsers.retrieve_replay, + BinaryAPIResponse, + ) class AsyncBrowsersResourceWithRawResponse: @@ -462,6 +550,10 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None: self.delete_by_id = async_to_raw_response_wrapper( browsers.delete_by_id, ) + self.retrieve_replay = async_to_custom_raw_response_wrapper( + browsers.retrieve_replay, + AsyncBinaryAPIResponse, + ) class BrowsersResourceWithStreamingResponse: @@ -483,6 +575,10 @@ def __init__(self, browsers: BrowsersResource) -> None: self.delete_by_id = to_streamed_response_wrapper( browsers.delete_by_id, ) + self.retrieve_replay = to_custom_streamed_response_wrapper( + browsers.retrieve_replay, + StreamedBinaryAPIResponse, + ) class AsyncBrowsersResourceWithStreamingResponse: @@ -504,3 +600,7 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None: self.delete_by_id = async_to_streamed_response_wrapper( browsers.delete_by_id, ) + self.retrieve_replay = async_to_custom_streamed_response_wrapper( + browsers.retrieve_replay, + AsyncStreamedBinaryAPIResponse, + ) diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index 746a92f9..7019e531 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -22,6 +22,9 @@ class BrowserCreateParams(TypedDict, total=False): persistence: BrowserPersistenceParam """Optional persistence configuration for the browser session.""" + replay: bool + """If true, enables replay recording of the browser session. Defaults to false.""" + stealth: bool """ If true, launches the browser in stealth mode to reduce detection by anti-bot diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index afba2b32..4fc470b1 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -23,3 +23,6 @@ class BrowserCreateResponse(BaseModel): persistence: Optional[BrowserPersistence] = None """Optional persistence configuration for the browser session.""" + + replay_view_url: Optional[str] = None + """Remote URL for viewing the browser session replay if enabled""" diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index 43c8d924..702c69b3 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -25,5 +25,8 @@ class BrowserListResponseItem(BaseModel): persistence: Optional[BrowserPersistence] = None """Optional persistence configuration for the browser session.""" + replay_view_url: Optional[str] = None + """Remote URL for viewing the browser session replay if enabled""" + BrowserListResponse: TypeAlias = List[BrowserListResponseItem] diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 45cf74b1..8f44ddb1 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -23,3 +23,6 @@ class BrowserRetrieveResponse(BaseModel): persistence: Optional[BrowserPersistence] = None """Optional persistence configuration for the browser session.""" + + replay_view_url: Optional[str] = None + """Remote URL for viewing the browser session replay if enabled""" diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index 8f990bea..a9cb8a55 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -5,7 +5,9 @@ import os from typing import Any, cast +import httpx import pytest +from respx import MockRouter from kernel import Kernel, AsyncKernel from tests.utils import assert_matches_type @@ -14,6 +16,12 @@ BrowserCreateResponse, BrowserRetrieveResponse, ) +from kernel._response import ( + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, +) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -34,6 +42,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: headless=False, invocation_id="rr33xuugxj9h0bkf1rdt2bet", persistence={"id": "my-awesome-browser-for-user-1234"}, + replay=True, stealth=True, ) assert_matches_type(BrowserCreateResponse, browser, path=["response"]) @@ -206,6 +215,66 @@ def test_path_params_delete_by_id(self, client: Kernel) -> None: "", ) + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_method_retrieve_replay(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/replay").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) + browser = client.browsers.retrieve_replay( + "id", + ) + assert browser.is_closed + assert browser.json() == {"foo": "bar"} + assert cast(Any, browser.is_closed) is True + assert isinstance(browser, BinaryAPIResponse) + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_raw_response_retrieve_replay(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/replay").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) + + browser = client.browsers.with_raw_response.retrieve_replay( + "id", + ) + + assert browser.is_closed is True + assert browser.http_request.headers.get("X-Stainless-Lang") == "python" + assert browser.json() == {"foo": "bar"} + assert isinstance(browser, BinaryAPIResponse) + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_streaming_response_retrieve_replay(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/replay").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) + with client.browsers.with_streaming_response.retrieve_replay( + "id", + ) as browser: + assert not browser.is_closed + assert browser.http_request.headers.get("X-Stainless-Lang") == "python" + + assert browser.json() == {"foo": "bar"} + assert cast(Any, browser.is_closed) is True + assert isinstance(browser, StreamedBinaryAPIResponse) + + assert cast(Any, browser.is_closed) is True + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_path_params_retrieve_replay(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.with_raw_response.retrieve_replay( + "", + ) + class TestAsyncBrowsers: parametrize = pytest.mark.parametrize( @@ -225,6 +294,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> headless=False, invocation_id="rr33xuugxj9h0bkf1rdt2bet", persistence={"id": "my-awesome-browser-for-user-1234"}, + replay=True, stealth=True, ) assert_matches_type(BrowserCreateResponse, browser, path=["response"]) @@ -396,3 +466,63 @@ async def test_path_params_delete_by_id(self, async_client: AsyncKernel) -> None await async_client.browsers.with_raw_response.delete_by_id( "", ) + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_method_retrieve_replay(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/replay").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) + browser = await async_client.browsers.retrieve_replay( + "id", + ) + assert browser.is_closed + assert await browser.json() == {"foo": "bar"} + assert cast(Any, browser.is_closed) is True + assert isinstance(browser, AsyncBinaryAPIResponse) + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_raw_response_retrieve_replay(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/replay").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) + + browser = await async_client.browsers.with_raw_response.retrieve_replay( + "id", + ) + + assert browser.is_closed is True + assert browser.http_request.headers.get("X-Stainless-Lang") == "python" + assert await browser.json() == {"foo": "bar"} + assert isinstance(browser, AsyncBinaryAPIResponse) + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_streaming_response_retrieve_replay(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/replay").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) + async with async_client.browsers.with_streaming_response.retrieve_replay( + "id", + ) as browser: + assert not browser.is_closed + assert browser.http_request.headers.get("X-Stainless-Lang") == "python" + + assert await browser.json() == {"foo": "bar"} + assert cast(Any, browser.is_closed) is True + assert isinstance(browser, AsyncStreamedBinaryAPIResponse) + + assert cast(Any, browser.is_closed) is True + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_path_params_retrieve_replay(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.with_raw_response.retrieve_replay( + "", + ) From f604d551fc25a977a2ea82b25a3e6420e864aa4d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 8 Jul 2025 19:01:58 +0000 Subject: [PATCH 116/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 12aa8969..1bc57136 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.6.4" + ".": "0.7.1" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 0c6e6abf..a5baacd4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.6.4" +version = "0.7.1" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index e0e69ec6..c3ceacf8 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.6.4" # x-release-please-version +__version__ = "0.7.1" # x-release-please-version From 7d2d1c1a6d49503f82e3ca7578252a40a4046839 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 02:29:44 +0000 Subject: [PATCH 117/448] chore(internal): bump pinned h11 dep --- requirements-dev.lock | 4 ++-- requirements.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements-dev.lock b/requirements-dev.lock index f4090db6..55681a90 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -48,9 +48,9 @@ filelock==3.12.4 frozenlist==1.6.2 # via aiohttp # via aiosignal -h11==0.14.0 +h11==0.16.0 # via httpcore -httpcore==1.0.2 +httpcore==1.0.9 # via httpx httpx==0.28.1 # via httpx-aiohttp diff --git a/requirements.lock b/requirements.lock index c125a9e6..61c4c7ac 100644 --- a/requirements.lock +++ b/requirements.lock @@ -36,9 +36,9 @@ exceptiongroup==1.2.2 frozenlist==1.6.2 # via aiohttp # via aiosignal -h11==0.14.0 +h11==0.16.0 # via httpcore -httpcore==1.0.2 +httpcore==1.0.9 # via httpx httpx==0.28.1 # via httpx-aiohttp From 765c8ad015e04cd3b588bcab353401823bc9563f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 02:48:37 +0000 Subject: [PATCH 118/448] chore(package): mark python 3.13 as supported --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index a5baacd4..e97bd2fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: MacOS", From 70c16524a8c3e9e79a127916b16829a7fe0f640d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 10 Jul 2025 02:59:21 +0000 Subject: [PATCH 119/448] fix(parsing): correctly handle nested discriminated unions --- src/kernel/_models.py | 13 ++++++++----- tests/test_models.py | 45 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/src/kernel/_models.py b/src/kernel/_models.py index 4f214980..528d5680 100644 --- a/src/kernel/_models.py +++ b/src/kernel/_models.py @@ -2,9 +2,10 @@ import os import inspect -from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, cast +from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast from datetime import date, datetime from typing_extensions import ( + List, Unpack, Literal, ClassVar, @@ -366,7 +367,7 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object: if type_ is None: raise RuntimeError(f"Unexpected field type is None for {key}") - return construct_type(value=value, type_=type_) + return construct_type(value=value, type_=type_, metadata=getattr(field, "metadata", None)) def is_basemodel(type_: type) -> bool: @@ -420,7 +421,7 @@ def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T: return cast(_T, construct_type(value=value, type_=type_)) -def construct_type(*, value: object, type_: object) -> object: +def construct_type(*, value: object, type_: object, metadata: Optional[List[Any]] = None) -> object: """Loose coercion to the expected type with construction of nested values. If the given value does not match the expected type then it is returned as-is. @@ -438,8 +439,10 @@ def construct_type(*, value: object, type_: object) -> object: type_ = type_.__value__ # type: ignore[unreachable] # unwrap `Annotated[T, ...]` -> `T` - if is_annotated_type(type_): - meta: tuple[Any, ...] = get_args(type_)[1:] + if metadata is not None: + meta: tuple[Any, ...] = tuple(metadata) + elif is_annotated_type(type_): + meta = get_args(type_)[1:] type_ = extract_type_arg(type_, 0) else: meta = tuple() diff --git a/tests/test_models.py b/tests/test_models.py index 4f412178..1e7293a8 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -889,3 +889,48 @@ class ModelB(BaseModel): ) assert isinstance(m, ModelB) + + +def test_nested_discriminated_union() -> None: + class InnerType1(BaseModel): + type: Literal["type_1"] + + class InnerModel(BaseModel): + inner_value: str + + class InnerType2(BaseModel): + type: Literal["type_2"] + some_inner_model: InnerModel + + class Type1(BaseModel): + base_type: Literal["base_type_1"] + value: Annotated[ + Union[ + InnerType1, + InnerType2, + ], + PropertyInfo(discriminator="type"), + ] + + class Type2(BaseModel): + base_type: Literal["base_type_2"] + + T = Annotated[ + Union[ + Type1, + Type2, + ], + PropertyInfo(discriminator="base_type"), + ] + + model = construct_type( + type_=T, + value={ + "base_type": "base_type_1", + "value": { + "type": "type_2", + }, + }, + ) + assert isinstance(model, Type1) + assert isinstance(model.value, InnerType2) From 661c4779f59656fa66491ff55b44dfdc975df3e9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 11 Jul 2025 03:03:33 +0000 Subject: [PATCH 120/448] chore(readme): fix version rendering on pypi --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8ee8a728..6f5f6ff7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Kernel Python API library -[![PyPI version]()](https://pypi.org/project/kernel/) + +[![PyPI version](https://img.shields.io/pypi/v/kernel.svg?label=pypi%20(stable))](https://pypi.org/project/kernel/) The Kernel Python library provides convenient access to the Kernel REST API from any Python 3.8+ application. The library includes type definitions for all request params and response fields, From 40fe29c4ed2bc5a04c8c40a0de8296d7aa0216e6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 12 Jul 2025 02:09:07 +0000 Subject: [PATCH 121/448] fix(client): don't send Content-Type header on GET requests --- pyproject.toml | 2 +- src/kernel/_base_client.py | 11 +++++++++-- tests/test_client.py | 4 ++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e97bd2fe..47a3ae2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ Homepage = "https://github.com/onkernel/kernel-python-sdk" Repository = "https://github.com/onkernel/kernel-python-sdk" [project.optional-dependencies] -aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.6"] +aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.8"] [tool.rye] managed = true diff --git a/src/kernel/_base_client.py b/src/kernel/_base_client.py index c90f227a..a654874a 100644 --- a/src/kernel/_base_client.py +++ b/src/kernel/_base_client.py @@ -529,6 +529,15 @@ def _build_request( # work around https://github.com/encode/httpx/discussions/2880 kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")} + is_body_allowed = options.method.lower() != "get" + + if is_body_allowed: + kwargs["json"] = json_data if is_given(json_data) else None + kwargs["files"] = files + else: + headers.pop("Content-Type", None) + kwargs.pop("data", None) + # TODO: report this error to httpx return self._client.build_request( # pyright: ignore[reportUnknownMemberType] headers=headers, @@ -540,8 +549,6 @@ def _build_request( # so that passing a `TypedDict` doesn't cause an error. # https://github.com/microsoft/pyright/issues/3526#event-6715453066 params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None, - json=json_data if is_given(json_data) else None, - files=files, **kwargs, ) diff --git a/tests/test_client.py b/tests/test_client.py index e58799c3..86a87907 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -462,7 +462,7 @@ def test_request_extra_query(self) -> None: def test_multipart_repeating_array(self, client: Kernel) -> None: request = client._build_request( FinalRequestOptions.construct( - method="get", + method="post", url="/foo", headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"}, json_data={"array": ["foo", "bar"]}, @@ -1271,7 +1271,7 @@ def test_request_extra_query(self) -> None: def test_multipart_repeating_array(self, async_client: AsyncKernel) -> None: request = async_client._build_request( FinalRequestOptions.construct( - method="get", + method="post", url="/foo", headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"}, json_data={"array": ["foo", "bar"]}, From 74216c61d13c2224934530000f872a1d40ddd31d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 02:08:45 +0000 Subject: [PATCH 122/448] feat: clean up environment call outs --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 6f5f6ff7..5041da06 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,6 @@ pip install kernel[aiohttp] Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: ```python -import os import asyncio from kernel import DefaultAioHttpClient from kernel import AsyncKernel @@ -101,7 +100,7 @@ from kernel import AsyncKernel async def main() -> None: async with AsyncKernel( - api_key=os.environ.get("KERNEL_API_KEY"), # This is the default and can be omitted + api_key="My API Key", http_client=DefaultAioHttpClient(), ) as client: deployment = await client.apps.deployments.create( From fc256e8b6ab25463cbdc7da1484a7fcb278918ef Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 16 Jul 2025 16:24:36 +0000 Subject: [PATCH 123/448] feat(api): manual updates replays! --- .stats.yml | 8 +- api.md | 26 +- src/kernel/_client.py | 3 +- src/kernel/resources/browsers/__init__.py | 33 ++ .../resources/{ => browsers}/browsers.py | 154 ++---- src/kernel/resources/browsers/replays.py | 454 ++++++++++++++++++ src/kernel/types/browser_create_params.py | 3 - src/kernel/types/browser_create_response.py | 3 - src/kernel/types/browser_list_response.py | 3 - src/kernel/types/browser_retrieve_response.py | 3 - src/kernel/types/browsers/__init__.py | 7 + .../types/browsers/replay_list_response.py | 26 + .../types/browsers/replay_start_params.py | 15 + .../types/browsers/replay_start_response.py | 22 + tests/api_resources/browsers/__init__.py | 1 + tests/api_resources/browsers/test_replays.py | 452 +++++++++++++++++ tests/api_resources/test_browsers.py | 130 ----- 17 files changed, 1079 insertions(+), 264 deletions(-) create mode 100644 src/kernel/resources/browsers/__init__.py rename src/kernel/resources/{ => browsers}/browsers.py (80%) create mode 100644 src/kernel/resources/browsers/replays.py create mode 100644 src/kernel/types/browsers/__init__.py create mode 100644 src/kernel/types/browsers/replay_list_response.py create mode 100644 src/kernel/types/browsers/replay_start_params.py create mode 100644 src/kernel/types/browsers/replay_start_response.py create mode 100644 tests/api_resources/browsers/__init__.py create mode 100644 tests/api_resources/browsers/test_replays.py diff --git a/.stats.yml b/.stats.yml index aa1aff73..a0553b1e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-d173129101e26f450c200e84430d993479c034700cf826917425d513b88912e6.yml -openapi_spec_hash: 150b86da7588979d7619b1a894e4720c -config_hash: eaeed470b1070b34df69c49d68e67355 +configured_endpoints: 21 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-a5b1d2c806c42c1534eefc8d34516f7f6e4ab68cb6a836534ee549bdbe4653f3.yml +openapi_spec_hash: 0be350cc8ddbd1fc7e058ce6c3a44ee8 +config_hash: 307153ecd5b85f77ce8e0d87f6e5dfab diff --git a/api.md b/api.md index 8a0fbd3e..58ceeacc 100644 --- a/api.md +++ b/api.md @@ -87,9 +87,23 @@ from kernel.types import ( Methods: -- client.browsers.create(\*\*params) -> BrowserCreateResponse -- client.browsers.retrieve(id) -> BrowserRetrieveResponse -- client.browsers.list() -> BrowserListResponse -- client.browsers.delete(\*\*params) -> None -- client.browsers.delete_by_id(id) -> None -- client.browsers.retrieve_replay(id) -> BinaryAPIResponse +- client.browsers.create(\*\*params) -> BrowserCreateResponse +- client.browsers.retrieve(id) -> BrowserRetrieveResponse +- client.browsers.list() -> BrowserListResponse +- client.browsers.delete(\*\*params) -> None +- client.browsers.delete_by_id(id) -> None + +## Replays + +Types: + +```python +from kernel.types.browsers import ReplayListResponse, ReplayStartResponse +``` + +Methods: + +- client.browsers.replays.list(id) -> ReplayListResponse +- client.browsers.replays.download(replay_id, \*, id) -> BinaryAPIResponse +- client.browsers.replays.start(id, \*\*params) -> ReplayStartResponse +- client.browsers.replays.stop(replay_id, \*, id) -> None diff --git a/src/kernel/_client.py b/src/kernel/_client.py index 63a7dc92..00510763 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -21,7 +21,7 @@ ) from ._utils import is_given, get_async_library from ._version import __version__ -from .resources import browsers, deployments, invocations +from .resources import deployments, invocations from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import KernelError, APIStatusError from ._base_client import ( @@ -30,6 +30,7 @@ AsyncAPIClient, ) from .resources.apps import apps +from .resources.browsers import browsers __all__ = [ "ENVIRONMENTS", diff --git a/src/kernel/resources/browsers/__init__.py b/src/kernel/resources/browsers/__init__.py new file mode 100644 index 00000000..236b5f71 --- /dev/null +++ b/src/kernel/resources/browsers/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .replays import ( + ReplaysResource, + AsyncReplaysResource, + ReplaysResourceWithRawResponse, + AsyncReplaysResourceWithRawResponse, + ReplaysResourceWithStreamingResponse, + AsyncReplaysResourceWithStreamingResponse, +) +from .browsers import ( + BrowsersResource, + AsyncBrowsersResource, + BrowsersResourceWithRawResponse, + AsyncBrowsersResourceWithRawResponse, + BrowsersResourceWithStreamingResponse, + AsyncBrowsersResourceWithStreamingResponse, +) + +__all__ = [ + "ReplaysResource", + "AsyncReplaysResource", + "ReplaysResourceWithRawResponse", + "AsyncReplaysResourceWithRawResponse", + "ReplaysResourceWithStreamingResponse", + "AsyncReplaysResourceWithStreamingResponse", + "BrowsersResource", + "AsyncBrowsersResource", + "BrowsersResourceWithRawResponse", + "AsyncBrowsersResourceWithRawResponse", + "BrowsersResourceWithStreamingResponse", + "AsyncBrowsersResourceWithStreamingResponse", +] diff --git a/src/kernel/resources/browsers.py b/src/kernel/resources/browsers/browsers.py similarity index 80% rename from src/kernel/resources/browsers.py rename to src/kernel/resources/browsers/browsers.py index 01b529bb..b44573e5 100644 --- a/src/kernel/resources/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -4,35 +4,39 @@ import httpx -from ..types import browser_create_params, browser_delete_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 ( - BinaryAPIResponse, - AsyncBinaryAPIResponse, - StreamedBinaryAPIResponse, - AsyncStreamedBinaryAPIResponse, +from ...types import browser_create_params, browser_delete_params +from .replays import ( + ReplaysResource, + AsyncReplaysResource, + ReplaysResourceWithRawResponse, + AsyncReplaysResourceWithRawResponse, + ReplaysResourceWithStreamingResponse, + AsyncReplaysResourceWithStreamingResponse, +) +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, - to_custom_raw_response_wrapper, async_to_streamed_response_wrapper, - to_custom_streamed_response_wrapper, - async_to_custom_raw_response_wrapper, - async_to_custom_streamed_response_wrapper, ) -from .._base_client import make_request_options -from ..types.browser_list_response import BrowserListResponse -from ..types.browser_create_response import BrowserCreateResponse -from ..types.browser_persistence_param import BrowserPersistenceParam -from ..types.browser_retrieve_response import BrowserRetrieveResponse +from ..._base_client import make_request_options +from ...types.browser_list_response import BrowserListResponse +from ...types.browser_create_response import BrowserCreateResponse +from ...types.browser_persistence_param import BrowserPersistenceParam +from ...types.browser_retrieve_response import BrowserRetrieveResponse __all__ = ["BrowsersResource", "AsyncBrowsersResource"] class BrowsersResource(SyncAPIResource): + @cached_property + def replays(self) -> ReplaysResource: + return ReplaysResource(self._client) + @cached_property def with_raw_response(self) -> BrowsersResourceWithRawResponse: """ @@ -58,7 +62,6 @@ def create( headless: bool | NotGiven = NOT_GIVEN, invocation_id: str | NotGiven = NOT_GIVEN, persistence: BrowserPersistenceParam | NotGiven = NOT_GIVEN, - replay: bool | NotGiven = NOT_GIVEN, stealth: 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. @@ -78,8 +81,6 @@ def create( persistence: Optional persistence configuration for the browser session. - replay: If true, enables replay recording of the browser session. Defaults to false. - stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. @@ -98,7 +99,6 @@ def create( "headless": headless, "invocation_id": invocation_id, "persistence": persistence, - "replay": replay, "stealth": stealth, }, browser_create_params.BrowserCreateParams, @@ -233,42 +233,12 @@ def delete_by_id( cast_to=NoneType, ) - def retrieve_replay( - self, - id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> BinaryAPIResponse: - """ - Get browser session replay. - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - extra_headers = {"Accept": "video/mp4", **(extra_headers or {})} - return self._get( - f"/browsers/{id}/replay", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=BinaryAPIResponse, - ) - class AsyncBrowsersResource(AsyncAPIResource): + @cached_property + def replays(self) -> AsyncReplaysResource: + return AsyncReplaysResource(self._client) + @cached_property def with_raw_response(self) -> AsyncBrowsersResourceWithRawResponse: """ @@ -294,7 +264,6 @@ async def create( headless: bool | NotGiven = NOT_GIVEN, invocation_id: str | NotGiven = NOT_GIVEN, persistence: BrowserPersistenceParam | NotGiven = NOT_GIVEN, - replay: bool | NotGiven = NOT_GIVEN, stealth: 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. @@ -314,8 +283,6 @@ async def create( persistence: Optional persistence configuration for the browser session. - replay: If true, enables replay recording of the browser session. Defaults to false. - stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. @@ -334,7 +301,6 @@ async def create( "headless": headless, "invocation_id": invocation_id, "persistence": persistence, - "replay": replay, "stealth": stealth, }, browser_create_params.BrowserCreateParams, @@ -471,40 +437,6 @@ async def delete_by_id( cast_to=NoneType, ) - async def retrieve_replay( - self, - id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AsyncBinaryAPIResponse: - """ - Get browser session replay. - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - extra_headers = {"Accept": "video/mp4", **(extra_headers or {})} - return await self._get( - f"/browsers/{id}/replay", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AsyncBinaryAPIResponse, - ) - class BrowsersResourceWithRawResponse: def __init__(self, browsers: BrowsersResource) -> None: @@ -525,10 +457,10 @@ def __init__(self, browsers: BrowsersResource) -> None: self.delete_by_id = to_raw_response_wrapper( browsers.delete_by_id, ) - self.retrieve_replay = to_custom_raw_response_wrapper( - browsers.retrieve_replay, - BinaryAPIResponse, - ) + + @cached_property + def replays(self) -> ReplaysResourceWithRawResponse: + return ReplaysResourceWithRawResponse(self._browsers.replays) class AsyncBrowsersResourceWithRawResponse: @@ -550,10 +482,10 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None: self.delete_by_id = async_to_raw_response_wrapper( browsers.delete_by_id, ) - self.retrieve_replay = async_to_custom_raw_response_wrapper( - browsers.retrieve_replay, - AsyncBinaryAPIResponse, - ) + + @cached_property + def replays(self) -> AsyncReplaysResourceWithRawResponse: + return AsyncReplaysResourceWithRawResponse(self._browsers.replays) class BrowsersResourceWithStreamingResponse: @@ -575,10 +507,10 @@ def __init__(self, browsers: BrowsersResource) -> None: self.delete_by_id = to_streamed_response_wrapper( browsers.delete_by_id, ) - self.retrieve_replay = to_custom_streamed_response_wrapper( - browsers.retrieve_replay, - StreamedBinaryAPIResponse, - ) + + @cached_property + def replays(self) -> ReplaysResourceWithStreamingResponse: + return ReplaysResourceWithStreamingResponse(self._browsers.replays) class AsyncBrowsersResourceWithStreamingResponse: @@ -600,7 +532,7 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None: self.delete_by_id = async_to_streamed_response_wrapper( browsers.delete_by_id, ) - self.retrieve_replay = async_to_custom_streamed_response_wrapper( - browsers.retrieve_replay, - AsyncStreamedBinaryAPIResponse, - ) + + @cached_property + def replays(self) -> AsyncReplaysResourceWithStreamingResponse: + return AsyncReplaysResourceWithStreamingResponse(self._browsers.replays) diff --git a/src/kernel/resources/browsers/replays.py b/src/kernel/resources/browsers/replays.py new file mode 100644 index 00000000..b801e8f0 --- /dev/null +++ b/src/kernel/resources/browsers/replays.py @@ -0,0 +1,454 @@ +# 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 ..._utils import maybe_transform, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + to_custom_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_custom_streamed_response_wrapper, + async_to_custom_raw_response_wrapper, + async_to_custom_streamed_response_wrapper, +) +from ..._base_client import make_request_options +from ...types.browsers import replay_start_params +from ...types.browsers.replay_list_response import ReplayListResponse +from ...types.browsers.replay_start_response import ReplayStartResponse + +__all__ = ["ReplaysResource", "AsyncReplaysResource"] + + +class ReplaysResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> ReplaysResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return ReplaysResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ReplaysResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return ReplaysResourceWithStreamingResponse(self) + + def list( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ReplayListResponse: + """ + List all replays for the specified browser session. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/browsers/{id}/replays", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ReplayListResponse, + ) + + def download( + self, + replay_id: str, + *, + id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BinaryAPIResponse: + """ + Download or stream the specified replay recording. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not replay_id: + raise ValueError(f"Expected a non-empty value for `replay_id` but received {replay_id!r}") + extra_headers = {"Accept": "video/mp4", **(extra_headers or {})} + return self._get( + f"/browsers/{id}/replays/{replay_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BinaryAPIResponse, + ) + + def start( + self, + id: str, + *, + framerate: int | NotGiven = NOT_GIVEN, + max_duration_in_seconds: 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, + ) -> ReplayStartResponse: + """ + Start recording the browser session and return a replay ID. + + Args: + framerate: Recording framerate in fps. + + max_duration_in_seconds: Maximum recording duration in seconds. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/browsers/{id}/replays", + body=maybe_transform( + { + "framerate": framerate, + "max_duration_in_seconds": max_duration_in_seconds, + }, + replay_start_params.ReplayStartParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ReplayStartResponse, + ) + + def stop( + self, + replay_id: str, + *, + id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """ + Stop the specified replay recording and persist the video. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not replay_id: + raise ValueError(f"Expected a non-empty value for `replay_id` but received {replay_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._post( + f"/browsers/{id}/replays/{replay_id}/stop", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class AsyncReplaysResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncReplaysResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncReplaysResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncReplaysResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return AsyncReplaysResourceWithStreamingResponse(self) + + async def list( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ReplayListResponse: + """ + List all replays for the specified browser session. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/browsers/{id}/replays", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ReplayListResponse, + ) + + async def download( + self, + replay_id: str, + *, + id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncBinaryAPIResponse: + """ + Download or stream the specified replay recording. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not replay_id: + raise ValueError(f"Expected a non-empty value for `replay_id` but received {replay_id!r}") + extra_headers = {"Accept": "video/mp4", **(extra_headers or {})} + return await self._get( + f"/browsers/{id}/replays/{replay_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AsyncBinaryAPIResponse, + ) + + async def start( + self, + id: str, + *, + framerate: int | NotGiven = NOT_GIVEN, + max_duration_in_seconds: 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, + ) -> ReplayStartResponse: + """ + Start recording the browser session and return a replay ID. + + Args: + framerate: Recording framerate in fps. + + max_duration_in_seconds: Maximum recording duration in seconds. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/browsers/{id}/replays", + body=await async_maybe_transform( + { + "framerate": framerate, + "max_duration_in_seconds": max_duration_in_seconds, + }, + replay_start_params.ReplayStartParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ReplayStartResponse, + ) + + async def stop( + self, + replay_id: str, + *, + id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """ + Stop the specified replay recording and persist the video. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not replay_id: + raise ValueError(f"Expected a non-empty value for `replay_id` but received {replay_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._post( + f"/browsers/{id}/replays/{replay_id}/stop", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class ReplaysResourceWithRawResponse: + def __init__(self, replays: ReplaysResource) -> None: + self._replays = replays + + self.list = to_raw_response_wrapper( + replays.list, + ) + self.download = to_custom_raw_response_wrapper( + replays.download, + BinaryAPIResponse, + ) + self.start = to_raw_response_wrapper( + replays.start, + ) + self.stop = to_raw_response_wrapper( + replays.stop, + ) + + +class AsyncReplaysResourceWithRawResponse: + def __init__(self, replays: AsyncReplaysResource) -> None: + self._replays = replays + + self.list = async_to_raw_response_wrapper( + replays.list, + ) + self.download = async_to_custom_raw_response_wrapper( + replays.download, + AsyncBinaryAPIResponse, + ) + self.start = async_to_raw_response_wrapper( + replays.start, + ) + self.stop = async_to_raw_response_wrapper( + replays.stop, + ) + + +class ReplaysResourceWithStreamingResponse: + def __init__(self, replays: ReplaysResource) -> None: + self._replays = replays + + self.list = to_streamed_response_wrapper( + replays.list, + ) + self.download = to_custom_streamed_response_wrapper( + replays.download, + StreamedBinaryAPIResponse, + ) + self.start = to_streamed_response_wrapper( + replays.start, + ) + self.stop = to_streamed_response_wrapper( + replays.stop, + ) + + +class AsyncReplaysResourceWithStreamingResponse: + def __init__(self, replays: AsyncReplaysResource) -> None: + self._replays = replays + + self.list = async_to_streamed_response_wrapper( + replays.list, + ) + self.download = async_to_custom_streamed_response_wrapper( + replays.download, + AsyncStreamedBinaryAPIResponse, + ) + self.start = async_to_streamed_response_wrapper( + replays.start, + ) + self.stop = async_to_streamed_response_wrapper( + replays.stop, + ) diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index 7019e531..746a92f9 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -22,9 +22,6 @@ class BrowserCreateParams(TypedDict, total=False): persistence: BrowserPersistenceParam """Optional persistence configuration for the browser session.""" - replay: bool - """If true, enables replay recording of the browser session. Defaults to false.""" - stealth: bool """ If true, launches the browser in stealth mode to reduce detection by anti-bot diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index 4fc470b1..afba2b32 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -23,6 +23,3 @@ class BrowserCreateResponse(BaseModel): persistence: Optional[BrowserPersistence] = None """Optional persistence configuration for the browser session.""" - - replay_view_url: Optional[str] = None - """Remote URL for viewing the browser session replay if enabled""" diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index 702c69b3..43c8d924 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -25,8 +25,5 @@ class BrowserListResponseItem(BaseModel): persistence: Optional[BrowserPersistence] = None """Optional persistence configuration for the browser session.""" - replay_view_url: Optional[str] = None - """Remote URL for viewing the browser session replay if enabled""" - BrowserListResponse: TypeAlias = List[BrowserListResponseItem] diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 8f44ddb1..45cf74b1 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -23,6 +23,3 @@ class BrowserRetrieveResponse(BaseModel): persistence: Optional[BrowserPersistence] = None """Optional persistence configuration for the browser session.""" - - replay_view_url: Optional[str] = None - """Remote URL for viewing the browser session replay if enabled""" diff --git a/src/kernel/types/browsers/__init__.py b/src/kernel/types/browsers/__init__.py new file mode 100644 index 00000000..61b18bc3 --- /dev/null +++ b/src/kernel/types/browsers/__init__.py @@ -0,0 +1,7 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .replay_start_params import ReplayStartParams as ReplayStartParams +from .replay_list_response import ReplayListResponse as ReplayListResponse +from .replay_start_response import ReplayStartResponse as ReplayStartResponse diff --git a/src/kernel/types/browsers/replay_list_response.py b/src/kernel/types/browsers/replay_list_response.py new file mode 100644 index 00000000..f53dd4d4 --- /dev/null +++ b/src/kernel/types/browsers/replay_list_response.py @@ -0,0 +1,26 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from datetime import datetime +from typing_extensions import TypeAlias + +from ..._models import BaseModel + +__all__ = ["ReplayListResponse", "ReplayListResponseItem"] + + +class ReplayListResponseItem(BaseModel): + replay_id: str + """Unique identifier for the replay recording.""" + + finished_at: Optional[datetime] = None + """Timestamp when replay finished""" + + replay_view_url: Optional[str] = None + """URL for viewing the replay recording.""" + + started_at: Optional[datetime] = None + """Timestamp when replay started""" + + +ReplayListResponse: TypeAlias = List[ReplayListResponseItem] diff --git a/src/kernel/types/browsers/replay_start_params.py b/src/kernel/types/browsers/replay_start_params.py new file mode 100644 index 00000000..d6683862 --- /dev/null +++ b/src/kernel/types/browsers/replay_start_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["ReplayStartParams"] + + +class ReplayStartParams(TypedDict, total=False): + framerate: int + """Recording framerate in fps.""" + + max_duration_in_seconds: int + """Maximum recording duration in seconds.""" diff --git a/src/kernel/types/browsers/replay_start_response.py b/src/kernel/types/browsers/replay_start_response.py new file mode 100644 index 00000000..dd837d50 --- /dev/null +++ b/src/kernel/types/browsers/replay_start_response.py @@ -0,0 +1,22 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime + +from ..._models import BaseModel + +__all__ = ["ReplayStartResponse"] + + +class ReplayStartResponse(BaseModel): + replay_id: str + """Unique identifier for the replay recording.""" + + finished_at: Optional[datetime] = None + """Timestamp when replay finished""" + + replay_view_url: Optional[str] = None + """URL for viewing the replay recording.""" + + started_at: Optional[datetime] = None + """Timestamp when replay started""" diff --git a/tests/api_resources/browsers/__init__.py b/tests/api_resources/browsers/__init__.py new file mode 100644 index 00000000..fd8019a9 --- /dev/null +++ b/tests/api_resources/browsers/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/browsers/test_replays.py b/tests/api_resources/browsers/test_replays.py new file mode 100644 index 00000000..930d008d --- /dev/null +++ b/tests/api_resources/browsers/test_replays.py @@ -0,0 +1,452 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import httpx +import pytest +from respx import MockRouter + +from kernel import Kernel, AsyncKernel +from tests.utils import assert_matches_type +from kernel._response import ( + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, +) +from kernel.types.browsers import ReplayListResponse, ReplayStartResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestReplays: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip() + @parametrize + def test_method_list(self, client: Kernel) -> None: + replay = client.browsers.replays.list( + "id", + ) + assert_matches_type(ReplayListResponse, replay, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_raw_response_list(self, client: Kernel) -> None: + response = client.browsers.replays.with_raw_response.list( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + replay = response.parse() + assert_matches_type(ReplayListResponse, replay, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_streaming_response_list(self, client: Kernel) -> None: + with client.browsers.replays.with_streaming_response.list( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + replay = response.parse() + assert_matches_type(ReplayListResponse, replay, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + def test_path_params_list(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.replays.with_raw_response.list( + "", + ) + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_method_download(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/id/replays/replay_id").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + replay = client.browsers.replays.download( + replay_id="replay_id", + id="id", + ) + assert replay.is_closed + assert replay.json() == {"foo": "bar"} + assert cast(Any, replay.is_closed) is True + assert isinstance(replay, BinaryAPIResponse) + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_raw_response_download(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/id/replays/replay_id").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + replay = client.browsers.replays.with_raw_response.download( + replay_id="replay_id", + id="id", + ) + + assert replay.is_closed is True + assert replay.http_request.headers.get("X-Stainless-Lang") == "python" + assert replay.json() == {"foo": "bar"} + assert isinstance(replay, BinaryAPIResponse) + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_streaming_response_download(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/id/replays/replay_id").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + with client.browsers.replays.with_streaming_response.download( + replay_id="replay_id", + id="id", + ) as replay: + assert not replay.is_closed + assert replay.http_request.headers.get("X-Stainless-Lang") == "python" + + assert replay.json() == {"foo": "bar"} + assert cast(Any, replay.is_closed) is True + assert isinstance(replay, StreamedBinaryAPIResponse) + + assert cast(Any, replay.is_closed) is True + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_path_params_download(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.replays.with_raw_response.download( + replay_id="replay_id", + id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `replay_id` but received ''"): + client.browsers.replays.with_raw_response.download( + replay_id="", + id="id", + ) + + @pytest.mark.skip() + @parametrize + def test_method_start(self, client: Kernel) -> None: + replay = client.browsers.replays.start( + id="id", + ) + assert_matches_type(ReplayStartResponse, replay, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_method_start_with_all_params(self, client: Kernel) -> None: + replay = client.browsers.replays.start( + id="id", + framerate=1, + max_duration_in_seconds=1, + ) + assert_matches_type(ReplayStartResponse, replay, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_raw_response_start(self, client: Kernel) -> None: + response = client.browsers.replays.with_raw_response.start( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + replay = response.parse() + assert_matches_type(ReplayStartResponse, replay, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_streaming_response_start(self, client: Kernel) -> None: + with client.browsers.replays.with_streaming_response.start( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + replay = response.parse() + assert_matches_type(ReplayStartResponse, replay, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + def test_path_params_start(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.replays.with_raw_response.start( + id="", + ) + + @pytest.mark.skip() + @parametrize + def test_method_stop(self, client: Kernel) -> None: + replay = client.browsers.replays.stop( + replay_id="replay_id", + id="id", + ) + assert replay is None + + @pytest.mark.skip() + @parametrize + def test_raw_response_stop(self, client: Kernel) -> None: + response = client.browsers.replays.with_raw_response.stop( + replay_id="replay_id", + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + replay = response.parse() + assert replay is None + + @pytest.mark.skip() + @parametrize + def test_streaming_response_stop(self, client: Kernel) -> None: + with client.browsers.replays.with_streaming_response.stop( + replay_id="replay_id", + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + replay = response.parse() + assert replay is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + def test_path_params_stop(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.replays.with_raw_response.stop( + replay_id="replay_id", + id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `replay_id` but received ''"): + client.browsers.replays.with_raw_response.stop( + replay_id="", + id="id", + ) + + +class TestAsyncReplays: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip() + @parametrize + async def test_method_list(self, async_client: AsyncKernel) -> None: + replay = await async_client.browsers.replays.list( + "id", + ) + assert_matches_type(ReplayListResponse, replay, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_raw_response_list(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.replays.with_raw_response.list( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + replay = await response.parse() + assert_matches_type(ReplayListResponse, replay, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.replays.with_streaming_response.list( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + replay = await response.parse() + assert_matches_type(ReplayListResponse, replay, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + async def test_path_params_list(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.replays.with_raw_response.list( + "", + ) + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_method_download(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/id/replays/replay_id").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + replay = await async_client.browsers.replays.download( + replay_id="replay_id", + id="id", + ) + assert replay.is_closed + assert await replay.json() == {"foo": "bar"} + assert cast(Any, replay.is_closed) is True + assert isinstance(replay, AsyncBinaryAPIResponse) + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_raw_response_download(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/id/replays/replay_id").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + replay = await async_client.browsers.replays.with_raw_response.download( + replay_id="replay_id", + id="id", + ) + + assert replay.is_closed is True + assert replay.http_request.headers.get("X-Stainless-Lang") == "python" + assert await replay.json() == {"foo": "bar"} + assert isinstance(replay, AsyncBinaryAPIResponse) + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_streaming_response_download(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/id/replays/replay_id").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + async with async_client.browsers.replays.with_streaming_response.download( + replay_id="replay_id", + id="id", + ) as replay: + assert not replay.is_closed + assert replay.http_request.headers.get("X-Stainless-Lang") == "python" + + assert await replay.json() == {"foo": "bar"} + assert cast(Any, replay.is_closed) is True + assert isinstance(replay, AsyncStreamedBinaryAPIResponse) + + assert cast(Any, replay.is_closed) is True + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_path_params_download(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.replays.with_raw_response.download( + replay_id="replay_id", + id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `replay_id` but received ''"): + await async_client.browsers.replays.with_raw_response.download( + replay_id="", + id="id", + ) + + @pytest.mark.skip() + @parametrize + async def test_method_start(self, async_client: AsyncKernel) -> None: + replay = await async_client.browsers.replays.start( + id="id", + ) + assert_matches_type(ReplayStartResponse, replay, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_method_start_with_all_params(self, async_client: AsyncKernel) -> None: + replay = await async_client.browsers.replays.start( + id="id", + framerate=1, + max_duration_in_seconds=1, + ) + assert_matches_type(ReplayStartResponse, replay, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_raw_response_start(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.replays.with_raw_response.start( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + replay = await response.parse() + assert_matches_type(ReplayStartResponse, replay, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_start(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.replays.with_streaming_response.start( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + replay = await response.parse() + assert_matches_type(ReplayStartResponse, replay, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + async def test_path_params_start(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.replays.with_raw_response.start( + id="", + ) + + @pytest.mark.skip() + @parametrize + async def test_method_stop(self, async_client: AsyncKernel) -> None: + replay = await async_client.browsers.replays.stop( + replay_id="replay_id", + id="id", + ) + assert replay is None + + @pytest.mark.skip() + @parametrize + async def test_raw_response_stop(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.replays.with_raw_response.stop( + replay_id="replay_id", + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + replay = await response.parse() + assert replay is None + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_stop(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.replays.with_streaming_response.stop( + replay_id="replay_id", + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + replay = await response.parse() + assert replay is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + async def test_path_params_stop(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.replays.with_raw_response.stop( + replay_id="replay_id", + id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `replay_id` but received ''"): + await async_client.browsers.replays.with_raw_response.stop( + replay_id="", + id="id", + ) diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index a9cb8a55..8f990bea 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -5,9 +5,7 @@ import os from typing import Any, cast -import httpx import pytest -from respx import MockRouter from kernel import Kernel, AsyncKernel from tests.utils import assert_matches_type @@ -16,12 +14,6 @@ BrowserCreateResponse, BrowserRetrieveResponse, ) -from kernel._response import ( - BinaryAPIResponse, - AsyncBinaryAPIResponse, - StreamedBinaryAPIResponse, - AsyncStreamedBinaryAPIResponse, -) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -42,7 +34,6 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: headless=False, invocation_id="rr33xuugxj9h0bkf1rdt2bet", persistence={"id": "my-awesome-browser-for-user-1234"}, - replay=True, stealth=True, ) assert_matches_type(BrowserCreateResponse, browser, path=["response"]) @@ -215,66 +206,6 @@ def test_path_params_delete_by_id(self, client: Kernel) -> None: "", ) - @pytest.mark.skip() - @parametrize - @pytest.mark.respx(base_url=base_url) - def test_method_retrieve_replay(self, client: Kernel, respx_mock: MockRouter) -> None: - respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/replay").mock( - return_value=httpx.Response(200, json={"foo": "bar"}) - ) - browser = client.browsers.retrieve_replay( - "id", - ) - assert browser.is_closed - assert browser.json() == {"foo": "bar"} - assert cast(Any, browser.is_closed) is True - assert isinstance(browser, BinaryAPIResponse) - - @pytest.mark.skip() - @parametrize - @pytest.mark.respx(base_url=base_url) - def test_raw_response_retrieve_replay(self, client: Kernel, respx_mock: MockRouter) -> None: - respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/replay").mock( - return_value=httpx.Response(200, json={"foo": "bar"}) - ) - - browser = client.browsers.with_raw_response.retrieve_replay( - "id", - ) - - assert browser.is_closed is True - assert browser.http_request.headers.get("X-Stainless-Lang") == "python" - assert browser.json() == {"foo": "bar"} - assert isinstance(browser, BinaryAPIResponse) - - @pytest.mark.skip() - @parametrize - @pytest.mark.respx(base_url=base_url) - def test_streaming_response_retrieve_replay(self, client: Kernel, respx_mock: MockRouter) -> None: - respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/replay").mock( - return_value=httpx.Response(200, json={"foo": "bar"}) - ) - with client.browsers.with_streaming_response.retrieve_replay( - "id", - ) as browser: - assert not browser.is_closed - assert browser.http_request.headers.get("X-Stainless-Lang") == "python" - - assert browser.json() == {"foo": "bar"} - assert cast(Any, browser.is_closed) is True - assert isinstance(browser, StreamedBinaryAPIResponse) - - assert cast(Any, browser.is_closed) is True - - @pytest.mark.skip() - @parametrize - @pytest.mark.respx(base_url=base_url) - def test_path_params_retrieve_replay(self, client: Kernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - client.browsers.with_raw_response.retrieve_replay( - "", - ) - class TestAsyncBrowsers: parametrize = pytest.mark.parametrize( @@ -294,7 +225,6 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> headless=False, invocation_id="rr33xuugxj9h0bkf1rdt2bet", persistence={"id": "my-awesome-browser-for-user-1234"}, - replay=True, stealth=True, ) assert_matches_type(BrowserCreateResponse, browser, path=["response"]) @@ -466,63 +396,3 @@ async def test_path_params_delete_by_id(self, async_client: AsyncKernel) -> None await async_client.browsers.with_raw_response.delete_by_id( "", ) - - @pytest.mark.skip() - @parametrize - @pytest.mark.respx(base_url=base_url) - async def test_method_retrieve_replay(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: - respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/replay").mock( - return_value=httpx.Response(200, json={"foo": "bar"}) - ) - browser = await async_client.browsers.retrieve_replay( - "id", - ) - assert browser.is_closed - assert await browser.json() == {"foo": "bar"} - assert cast(Any, browser.is_closed) is True - assert isinstance(browser, AsyncBinaryAPIResponse) - - @pytest.mark.skip() - @parametrize - @pytest.mark.respx(base_url=base_url) - async def test_raw_response_retrieve_replay(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: - respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/replay").mock( - return_value=httpx.Response(200, json={"foo": "bar"}) - ) - - browser = await async_client.browsers.with_raw_response.retrieve_replay( - "id", - ) - - assert browser.is_closed is True - assert browser.http_request.headers.get("X-Stainless-Lang") == "python" - assert await browser.json() == {"foo": "bar"} - assert isinstance(browser, AsyncBinaryAPIResponse) - - @pytest.mark.skip() - @parametrize - @pytest.mark.respx(base_url=base_url) - async def test_streaming_response_retrieve_replay(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: - respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/replay").mock( - return_value=httpx.Response(200, json={"foo": "bar"}) - ) - async with async_client.browsers.with_streaming_response.retrieve_replay( - "id", - ) as browser: - assert not browser.is_closed - assert browser.http_request.headers.get("X-Stainless-Lang") == "python" - - assert await browser.json() == {"foo": "bar"} - assert cast(Any, browser.is_closed) is True - assert isinstance(browser, AsyncStreamedBinaryAPIResponse) - - assert cast(Any, browser.is_closed) is True - - @pytest.mark.skip() - @parametrize - @pytest.mark.respx(base_url=base_url) - async def test_path_params_retrieve_replay(self, async_client: AsyncKernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - await async_client.browsers.with_raw_response.retrieve_replay( - "", - ) From 659286283521d03d7a60680b6651652608a461eb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 16 Jul 2025 16:33:05 +0000 Subject: [PATCH 124/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 1bc57136..6538ca91 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.7.1" + ".": "0.8.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 47a3ae2c..0eb2b247 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.7.1" +version = "0.8.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index c3ceacf8..73e122d0 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.7.1" # x-release-please-version +__version__ = "0.8.0" # x-release-please-version From c35a5a6f80860f4c86ff840c7939e16cb892e0c4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:51:38 +0000 Subject: [PATCH 125/448] chore(api): remove deprecated endpoints --- .stats.yml | 8 +- README.md | 34 +- api.md | 15 +- src/kernel/_client.py | 3 +- src/kernel/resources/{apps => }/apps.py | 48 +-- src/kernel/resources/apps/__init__.py | 33 -- src/kernel/resources/apps/deployments.py | 328 ------------------ src/kernel/types/apps/__init__.py | 7 - .../types/apps/deployment_create_params.py | 33 -- .../types/apps/deployment_create_response.py | 31 -- .../types/apps/deployment_follow_response.py | 41 --- tests/api_resources/apps/__init__.py | 1 - tests/api_resources/apps/test_deployments.py | 222 ------------ 13 files changed, 24 insertions(+), 780 deletions(-) rename src/kernel/resources/{apps => }/apps.py (79%) delete mode 100644 src/kernel/resources/apps/__init__.py delete mode 100644 src/kernel/resources/apps/deployments.py delete mode 100644 src/kernel/types/apps/__init__.py delete mode 100644 src/kernel/types/apps/deployment_create_params.py delete mode 100644 src/kernel/types/apps/deployment_create_response.py delete mode 100644 src/kernel/types/apps/deployment_follow_response.py delete mode 100644 tests/api_resources/apps/__init__.py delete mode 100644 tests/api_resources/apps/test_deployments.py diff --git a/.stats.yml b/.stats.yml index a0553b1e..38d124b5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-a5b1d2c806c42c1534eefc8d34516f7f6e4ab68cb6a836534ee549bdbe4653f3.yml -openapi_spec_hash: 0be350cc8ddbd1fc7e058ce6c3a44ee8 -config_hash: 307153ecd5b85f77ce8e0d87f6e5dfab +configured_endpoints: 19 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-84945582139b11633f792c1052a33e6af9cafc96bbafc2902a905312d14c4cc1.yml +openapi_spec_hash: c77be216626b789a543529a6de56faed +config_hash: 65328ff206b8c0168c915914506d9dba diff --git a/README.md b/README.md index 5041da06..884d10c2 100644 --- a/README.md +++ b/README.md @@ -34,13 +34,10 @@ client = Kernel( environment="development", ) -deployment = client.apps.deployments.create( - entrypoint_rel_path="main.ts", - file=b"REPLACE_ME", - env_vars={"OPENAI_API_KEY": "x"}, - version="1.0.0", +browser = client.browsers.create( + persistence={"id": "browser-for-user-1234"}, ) -print(deployment.apps) +print(browser.session_id) ``` While you can provide an `api_key` keyword argument, @@ -65,13 +62,10 @@ client = AsyncKernel( async def main() -> None: - deployment = await client.apps.deployments.create( - entrypoint_rel_path="main.ts", - file=b"REPLACE_ME", - env_vars={"OPENAI_API_KEY": "x"}, - version="1.0.0", + browser = await client.browsers.create( + persistence={"id": "browser-for-user-1234"}, ) - print(deployment.apps) + print(browser.session_id) asyncio.run(main()) @@ -103,13 +97,10 @@ async def main() -> None: api_key="My API Key", http_client=DefaultAioHttpClient(), ) as client: - deployment = await client.apps.deployments.create( - entrypoint_rel_path="main.ts", - file=b"REPLACE_ME", - env_vars={"OPENAI_API_KEY": "x"}, - version="1.0.0", + browser = await client.browsers.create( + persistence={"id": "browser-for-user-1234"}, ) - print(deployment.apps) + print(browser.session_id) asyncio.run(main()) @@ -149,7 +140,7 @@ from kernel import Kernel client = Kernel() -client.apps.deployments.create( +client.deployments.create( entrypoint_rel_path="src/app.py", file=Path("/path/to/file"), ) @@ -174,7 +165,6 @@ client = Kernel() try: client.browsers.create( - invocation_id="REPLACE_ME", persistence={"id": "browser-for-user-1234"}, ) except kernel.APIConnectionError as e: @@ -220,7 +210,6 @@ client = Kernel( # Or, configure per-request: client.with_options(max_retries=5).browsers.create( - invocation_id="REPLACE_ME", persistence={"id": "browser-for-user-1234"}, ) ``` @@ -246,7 +235,6 @@ client = Kernel( # Override per-request: client.with_options(timeout=5.0).browsers.create( - invocation_id="REPLACE_ME", persistence={"id": "browser-for-user-1234"}, ) ``` @@ -290,7 +278,6 @@ from kernel import Kernel client = Kernel() response = client.browsers.with_raw_response.create( - invocation_id="REPLACE_ME", persistence={ "id": "browser-for-user-1234" }, @@ -313,7 +300,6 @@ To stream the response body, use `.with_streaming_response` instead, which requi ```python with client.browsers.with_streaming_response.create( - invocation_id="REPLACE_ME", persistence={"id": "browser-for-user-1234"}, ) as response: print(response.headers.get("X-My-Header")) diff --git a/api.md b/api.md index 58ceeacc..434ace20 100644 --- a/api.md +++ b/api.md @@ -35,20 +35,7 @@ from kernel.types import AppListResponse Methods: -- client.apps.list(\*\*params) -> AppListResponse - -## Deployments - -Types: - -```python -from kernel.types.apps import DeploymentCreateResponse, DeploymentFollowResponse -``` - -Methods: - -- client.apps.deployments.create(\*\*params) -> DeploymentCreateResponse -- client.apps.deployments.follow(id) -> DeploymentFollowResponse +- client.apps.list(\*\*params) -> AppListResponse # Invocations diff --git a/src/kernel/_client.py b/src/kernel/_client.py index 00510763..a0b9ec29 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -21,7 +21,7 @@ ) from ._utils import is_given, get_async_library from ._version import __version__ -from .resources import deployments, invocations +from .resources import apps, deployments, invocations from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import KernelError, APIStatusError from ._base_client import ( @@ -29,7 +29,6 @@ SyncAPIClient, AsyncAPIClient, ) -from .resources.apps import apps from .resources.browsers import browsers __all__ = [ diff --git a/src/kernel/resources/apps/apps.py b/src/kernel/resources/apps.py similarity index 79% rename from src/kernel/resources/apps/apps.py rename to src/kernel/resources/apps.py index 726db204..652235e2 100644 --- a/src/kernel/resources/apps/apps.py +++ b/src/kernel/resources/apps.py @@ -4,36 +4,24 @@ import httpx -from ...types import app_list_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 ( +from ..types import app_list_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 .deployments import ( - DeploymentsResource, - AsyncDeploymentsResource, - DeploymentsResourceWithRawResponse, - AsyncDeploymentsResourceWithRawResponse, - DeploymentsResourceWithStreamingResponse, - AsyncDeploymentsResourceWithStreamingResponse, -) -from ..._base_client import make_request_options -from ...types.app_list_response import AppListResponse +from .._base_client import make_request_options +from ..types.app_list_response import AppListResponse __all__ = ["AppsResource", "AsyncAppsResource"] class AppsResource(SyncAPIResource): - @cached_property - def deployments(self) -> DeploymentsResource: - return DeploymentsResource(self._client) - @cached_property def with_raw_response(self) -> AppsResourceWithRawResponse: """ @@ -102,10 +90,6 @@ def list( class AsyncAppsResource(AsyncAPIResource): - @cached_property - def deployments(self) -> AsyncDeploymentsResource: - return AsyncDeploymentsResource(self._client) - @cached_property def with_raw_response(self) -> AsyncAppsResourceWithRawResponse: """ @@ -181,10 +165,6 @@ def __init__(self, apps: AppsResource) -> None: apps.list, ) - @cached_property - def deployments(self) -> DeploymentsResourceWithRawResponse: - return DeploymentsResourceWithRawResponse(self._apps.deployments) - class AsyncAppsResourceWithRawResponse: def __init__(self, apps: AsyncAppsResource) -> None: @@ -194,10 +174,6 @@ def __init__(self, apps: AsyncAppsResource) -> None: apps.list, ) - @cached_property - def deployments(self) -> AsyncDeploymentsResourceWithRawResponse: - return AsyncDeploymentsResourceWithRawResponse(self._apps.deployments) - class AppsResourceWithStreamingResponse: def __init__(self, apps: AppsResource) -> None: @@ -207,10 +183,6 @@ def __init__(self, apps: AppsResource) -> None: apps.list, ) - @cached_property - def deployments(self) -> DeploymentsResourceWithStreamingResponse: - return DeploymentsResourceWithStreamingResponse(self._apps.deployments) - class AsyncAppsResourceWithStreamingResponse: def __init__(self, apps: AsyncAppsResource) -> None: @@ -219,7 +191,3 @@ def __init__(self, apps: AsyncAppsResource) -> None: self.list = async_to_streamed_response_wrapper( apps.list, ) - - @cached_property - def deployments(self) -> AsyncDeploymentsResourceWithStreamingResponse: - return AsyncDeploymentsResourceWithStreamingResponse(self._apps.deployments) diff --git a/src/kernel/resources/apps/__init__.py b/src/kernel/resources/apps/__init__.py deleted file mode 100644 index 6ce731d2..00000000 --- a/src/kernel/resources/apps/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from .apps import ( - AppsResource, - AsyncAppsResource, - AppsResourceWithRawResponse, - AsyncAppsResourceWithRawResponse, - AppsResourceWithStreamingResponse, - AsyncAppsResourceWithStreamingResponse, -) -from .deployments import ( - DeploymentsResource, - AsyncDeploymentsResource, - DeploymentsResourceWithRawResponse, - AsyncDeploymentsResourceWithRawResponse, - DeploymentsResourceWithStreamingResponse, - AsyncDeploymentsResourceWithStreamingResponse, -) - -__all__ = [ - "DeploymentsResource", - "AsyncDeploymentsResource", - "DeploymentsResourceWithRawResponse", - "AsyncDeploymentsResourceWithRawResponse", - "DeploymentsResourceWithStreamingResponse", - "AsyncDeploymentsResourceWithStreamingResponse", - "AppsResource", - "AsyncAppsResource", - "AppsResourceWithRawResponse", - "AsyncAppsResourceWithRawResponse", - "AppsResourceWithStreamingResponse", - "AsyncAppsResourceWithStreamingResponse", -] diff --git a/src/kernel/resources/apps/deployments.py b/src/kernel/resources/apps/deployments.py deleted file mode 100644 index 98d1728c..00000000 --- a/src/kernel/resources/apps/deployments.py +++ /dev/null @@ -1,328 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Any, Dict, Mapping, cast -from typing_extensions import Literal - -import httpx - -from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes -from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform -from ..._compat import cached_property -from ..._resource import SyncAPIResource, AsyncAPIResource -from ..._response import ( - to_raw_response_wrapper, - to_streamed_response_wrapper, - async_to_raw_response_wrapper, - async_to_streamed_response_wrapper, -) -from ..._streaming import Stream, AsyncStream -from ...types.apps import deployment_create_params -from ..._base_client import make_request_options -from ...types.apps.deployment_create_response import DeploymentCreateResponse -from ...types.apps.deployment_follow_response import DeploymentFollowResponse - -__all__ = ["DeploymentsResource", "AsyncDeploymentsResource"] - - -class DeploymentsResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> DeploymentsResourceWithRawResponse: - """ - 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers - """ - return DeploymentsResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> DeploymentsResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response - """ - return DeploymentsResourceWithStreamingResponse(self) - - def create( - self, - *, - entrypoint_rel_path: str, - file: FileTypes, - env_vars: Dict[str, str] | NotGiven = NOT_GIVEN, - force: bool | NotGiven = NOT_GIVEN, - region: Literal["aws.us-east-1a"] | NotGiven = NOT_GIVEN, - version: 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, - ) -> DeploymentCreateResponse: - """ - Deploy a new application and associated actions to Kernel. - - Args: - entrypoint_rel_path: Relative path to the entrypoint of the application - - file: ZIP file containing the application source directory - - env_vars: Map of environment variables to set for the deployed application. Each key-value - pair represents an environment variable. - - force: Allow overwriting an existing app version - - region: Region for deployment. Currently we only support "aws.us-east-1a" - - version: Version of the application. Can be any string. - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - body = deepcopy_minimal( - { - "entrypoint_rel_path": entrypoint_rel_path, - "file": file, - "env_vars": env_vars, - "force": force, - "region": region, - "version": version, - } - ) - files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) - # It should be noted that the actual Content-Type header that will be - # sent to the server will contain a `boundary` parameter, e.g. - # multipart/form-data; boundary=---abc-- - extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} - return self._post( - "/deploy", - body=maybe_transform(body, deployment_create_params.DeploymentCreateParams), - files=files, - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=DeploymentCreateResponse, - ) - - def follow( - self, - id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Stream[DeploymentFollowResponse]: - """ - Establishes a Server-Sent Events (SSE) stream that delivers real-time logs and - status updates for a deployed application. The stream terminates automatically - once the application reaches a terminal state. - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} - return self._get( - f"/apps/{id}/events", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=cast( - Any, DeploymentFollowResponse - ), # Union types cannot be passed in as arguments in the type system - stream=True, - stream_cls=Stream[DeploymentFollowResponse], - ) - - -class AsyncDeploymentsResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncDeploymentsResourceWithRawResponse: - """ - 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers - """ - return AsyncDeploymentsResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncDeploymentsResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response - """ - return AsyncDeploymentsResourceWithStreamingResponse(self) - - async def create( - self, - *, - entrypoint_rel_path: str, - file: FileTypes, - env_vars: Dict[str, str] | NotGiven = NOT_GIVEN, - force: bool | NotGiven = NOT_GIVEN, - region: Literal["aws.us-east-1a"] | NotGiven = NOT_GIVEN, - version: 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, - ) -> DeploymentCreateResponse: - """ - Deploy a new application and associated actions to Kernel. - - Args: - entrypoint_rel_path: Relative path to the entrypoint of the application - - file: ZIP file containing the application source directory - - env_vars: Map of environment variables to set for the deployed application. Each key-value - pair represents an environment variable. - - force: Allow overwriting an existing app version - - region: Region for deployment. Currently we only support "aws.us-east-1a" - - version: Version of the application. Can be any string. - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - body = deepcopy_minimal( - { - "entrypoint_rel_path": entrypoint_rel_path, - "file": file, - "env_vars": env_vars, - "force": force, - "region": region, - "version": version, - } - ) - files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) - # It should be noted that the actual Content-Type header that will be - # sent to the server will contain a `boundary` parameter, e.g. - # multipart/form-data; boundary=---abc-- - extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} - return await self._post( - "/deploy", - body=await async_maybe_transform(body, deployment_create_params.DeploymentCreateParams), - files=files, - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=DeploymentCreateResponse, - ) - - async def follow( - self, - id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AsyncStream[DeploymentFollowResponse]: - """ - Establishes a Server-Sent Events (SSE) stream that delivers real-time logs and - status updates for a deployed application. The stream terminates automatically - once the application reaches a terminal state. - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} - return await self._get( - f"/apps/{id}/events", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=cast( - Any, DeploymentFollowResponse - ), # Union types cannot be passed in as arguments in the type system - stream=True, - stream_cls=AsyncStream[DeploymentFollowResponse], - ) - - -class DeploymentsResourceWithRawResponse: - def __init__(self, deployments: DeploymentsResource) -> None: - self._deployments = deployments - - self.create = to_raw_response_wrapper( - deployments.create, - ) - self.follow = to_raw_response_wrapper( - deployments.follow, - ) - - -class AsyncDeploymentsResourceWithRawResponse: - def __init__(self, deployments: AsyncDeploymentsResource) -> None: - self._deployments = deployments - - self.create = async_to_raw_response_wrapper( - deployments.create, - ) - self.follow = async_to_raw_response_wrapper( - deployments.follow, - ) - - -class DeploymentsResourceWithStreamingResponse: - def __init__(self, deployments: DeploymentsResource) -> None: - self._deployments = deployments - - self.create = to_streamed_response_wrapper( - deployments.create, - ) - self.follow = to_streamed_response_wrapper( - deployments.follow, - ) - - -class AsyncDeploymentsResourceWithStreamingResponse: - def __init__(self, deployments: AsyncDeploymentsResource) -> None: - self._deployments = deployments - - self.create = async_to_streamed_response_wrapper( - deployments.create, - ) - self.follow = async_to_streamed_response_wrapper( - deployments.follow, - ) diff --git a/src/kernel/types/apps/__init__.py b/src/kernel/types/apps/__init__.py deleted file mode 100644 index 93aed9dd..00000000 --- a/src/kernel/types/apps/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from .deployment_create_params import DeploymentCreateParams as DeploymentCreateParams -from .deployment_create_response import DeploymentCreateResponse as DeploymentCreateResponse -from .deployment_follow_response import DeploymentFollowResponse as DeploymentFollowResponse diff --git a/src/kernel/types/apps/deployment_create_params.py b/src/kernel/types/apps/deployment_create_params.py deleted file mode 100644 index cd1a7b53..00000000 --- a/src/kernel/types/apps/deployment_create_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 Dict -from typing_extensions import Literal, Required, TypedDict - -from ..._types import FileTypes - -__all__ = ["DeploymentCreateParams"] - - -class DeploymentCreateParams(TypedDict, total=False): - entrypoint_rel_path: Required[str] - """Relative path to the entrypoint of the application""" - - file: Required[FileTypes] - """ZIP file containing the application source directory""" - - env_vars: Dict[str, str] - """Map of environment variables to set for the deployed application. - - Each key-value pair represents an environment variable. - """ - - force: bool - """Allow overwriting an existing app version""" - - region: Literal["aws.us-east-1a"] - """Region for deployment. Currently we only support "aws.us-east-1a" """ - - version: str - """Version of the application. Can be any string.""" diff --git a/src/kernel/types/apps/deployment_create_response.py b/src/kernel/types/apps/deployment_create_response.py deleted file mode 100644 index 8696a0f7..00000000 --- a/src/kernel/types/apps/deployment_create_response.py +++ /dev/null @@ -1,31 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Optional -from typing_extensions import Literal - -from ..._models import BaseModel -from ..shared.app_action import AppAction - -__all__ = ["DeploymentCreateResponse", "App"] - - -class App(BaseModel): - id: str - """ID for the app version deployed""" - - actions: List[AppAction] - """List of actions available on the app""" - - name: str - """Name of the app""" - - -class DeploymentCreateResponse(BaseModel): - apps: List[App] - """List of apps deployed""" - - status: Literal["queued", "deploying", "succeeded", "failed"] - """Current status of the deployment""" - - status_reason: Optional[str] = None - """Status reason""" diff --git a/src/kernel/types/apps/deployment_follow_response.py b/src/kernel/types/apps/deployment_follow_response.py deleted file mode 100644 index 6a196961..00000000 --- a/src/kernel/types/apps/deployment_follow_response.py +++ /dev/null @@ -1,41 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Union, Optional -from datetime import datetime -from typing_extensions import Literal, Annotated, TypeAlias - -from ..._utils import PropertyInfo -from ..._models import BaseModel -from ..shared.log_event import LogEvent -from ..shared.heartbeat_event import HeartbeatEvent - -__all__ = ["DeploymentFollowResponse", "StateEvent", "StateUpdateEvent"] - - -class StateEvent(BaseModel): - event: Literal["state"] - """Event type identifier (always "state").""" - - state: str - """ - Current application state (e.g., "deploying", "running", "succeeded", "failed"). - """ - - timestamp: Optional[datetime] = None - """Time the state was reported.""" - - -class StateUpdateEvent(BaseModel): - event: Literal["state_update"] - """Event type identifier (always "state_update").""" - - state: str - """New application state (e.g., "running", "succeeded", "failed").""" - - timestamp: Optional[datetime] = None - """Time the state change occurred.""" - - -DeploymentFollowResponse: TypeAlias = Annotated[ - Union[StateEvent, StateUpdateEvent, LogEvent, HeartbeatEvent], PropertyInfo(discriminator="event") -] diff --git a/tests/api_resources/apps/__init__.py b/tests/api_resources/apps/__init__.py deleted file mode 100644 index fd8019a9..00000000 --- a/tests/api_resources/apps/__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/apps/test_deployments.py b/tests/api_resources/apps/test_deployments.py deleted file mode 100644 index 7b613c85..00000000 --- a/tests/api_resources/apps/test_deployments.py +++ /dev/null @@ -1,222 +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 kernel import Kernel, AsyncKernel -from tests.utils import assert_matches_type -from kernel.types.apps import DeploymentCreateResponse - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestDeployments: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip() - @parametrize - def test_method_create(self, client: Kernel) -> None: - deployment = client.apps.deployments.create( - entrypoint_rel_path="src/app.py", - file=b"raw file contents", - ) - assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) - - @pytest.mark.skip() - @parametrize - def test_method_create_with_all_params(self, client: Kernel) -> None: - deployment = client.apps.deployments.create( - entrypoint_rel_path="src/app.py", - file=b"raw file contents", - env_vars={"foo": "string"}, - force=False, - region="aws.us-east-1a", - version="1.0.0", - ) - assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) - - @pytest.mark.skip() - @parametrize - def test_raw_response_create(self, client: Kernel) -> None: - response = client.apps.deployments.with_raw_response.create( - entrypoint_rel_path="src/app.py", - file=b"raw file contents", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - deployment = response.parse() - assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) - - @pytest.mark.skip() - @parametrize - def test_streaming_response_create(self, client: Kernel) -> None: - with client.apps.deployments.with_streaming_response.create( - entrypoint_rel_path="src/app.py", - file=b"raw file contents", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - deployment = response.parse() - assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) - @parametrize - def test_method_follow(self, client: Kernel) -> None: - deployment_stream = client.apps.deployments.follow( - "id", - ) - deployment_stream.response.close() - - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) - @parametrize - def test_raw_response_follow(self, client: Kernel) -> None: - response = client.apps.deployments.with_raw_response.follow( - "id", - ) - - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - stream = response.parse() - stream.close() - - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) - @parametrize - def test_streaming_response_follow(self, client: Kernel) -> None: - with client.apps.deployments.with_streaming_response.follow( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - stream = response.parse() - stream.close() - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) - @parametrize - def test_path_params_follow(self, client: Kernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - client.apps.deployments.with_raw_response.follow( - "", - ) - - -class TestAsyncDeployments: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @pytest.mark.skip() - @parametrize - async def test_method_create(self, async_client: AsyncKernel) -> None: - deployment = await async_client.apps.deployments.create( - entrypoint_rel_path="src/app.py", - file=b"raw file contents", - ) - assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) - - @pytest.mark.skip() - @parametrize - async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: - deployment = await async_client.apps.deployments.create( - entrypoint_rel_path="src/app.py", - file=b"raw file contents", - env_vars={"foo": "string"}, - force=False, - region="aws.us-east-1a", - version="1.0.0", - ) - assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) - - @pytest.mark.skip() - @parametrize - async def test_raw_response_create(self, async_client: AsyncKernel) -> None: - response = await async_client.apps.deployments.with_raw_response.create( - entrypoint_rel_path="src/app.py", - file=b"raw file contents", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - deployment = await response.parse() - assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) - - @pytest.mark.skip() - @parametrize - async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: - async with async_client.apps.deployments.with_streaming_response.create( - entrypoint_rel_path="src/app.py", - file=b"raw file contents", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - deployment = await response.parse() - assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) - @parametrize - async def test_method_follow(self, async_client: AsyncKernel) -> None: - deployment_stream = await async_client.apps.deployments.follow( - "id", - ) - await deployment_stream.response.aclose() - - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) - @parametrize - async def test_raw_response_follow(self, async_client: AsyncKernel) -> None: - response = await async_client.apps.deployments.with_raw_response.follow( - "id", - ) - - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - stream = await response.parse() - await stream.close() - - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) - @parametrize - async def test_streaming_response_follow(self, async_client: AsyncKernel) -> None: - async with async_client.apps.deployments.with_streaming_response.follow( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - stream = await response.parse() - await stream.close() - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) - @parametrize - async def test_path_params_follow(self, async_client: AsyncKernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - await async_client.apps.deployments.with_raw_response.follow( - "", - ) From c6335a6505e193f09d8a0cf468733bce9242580d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 21 Jul 2025 18:36:52 +0000 Subject: [PATCH 126/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 6538ca91..2b28d6ec 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.8.0" + ".": "0.8.1" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 0eb2b247..83d7c720 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.8.0" +version = "0.8.1" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 73e122d0..8e3e5204 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.8.0" # x-release-please-version +__version__ = "0.8.1" # x-release-please-version From e5963e1c24a77c6d89d819109584eda0ffd8df48 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 22 Jul 2025 02:09:01 +0000 Subject: [PATCH 127/448] fix(parsing): ignore empty metadata --- src/kernel/_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kernel/_models.py b/src/kernel/_models.py index 528d5680..ffcbf67b 100644 --- a/src/kernel/_models.py +++ b/src/kernel/_models.py @@ -439,7 +439,7 @@ def construct_type(*, value: object, type_: object, metadata: Optional[List[Any] type_ = type_.__value__ # type: ignore[unreachable] # unwrap `Annotated[T, ...]` -> `T` - if metadata is not None: + if metadata is not None and len(metadata) > 0: meta: tuple[Any, ...] = tuple(metadata) elif is_annotated_type(type_): meta = get_args(type_)[1:] From 4c7d076f8e3feb82e285e265ae8d06987a07f3ad Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 02:12:35 +0000 Subject: [PATCH 128/448] fix(parsing): parse extra field types --- src/kernel/_models.py | 25 +++++++++++++++++++++++-- tests/test_models.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/kernel/_models.py b/src/kernel/_models.py index ffcbf67b..b8387ce9 100644 --- a/src/kernel/_models.py +++ b/src/kernel/_models.py @@ -208,14 +208,18 @@ def construct( # pyright: ignore[reportIncompatibleMethodOverride] else: fields_values[name] = field_get_default(field) + extra_field_type = _get_extra_fields_type(__cls) + _extra = {} for key, value in values.items(): if key not in model_fields: + parsed = construct_type(value=value, type_=extra_field_type) if extra_field_type is not None else value + if PYDANTIC_V2: - _extra[key] = value + _extra[key] = parsed else: _fields_set.add(key) - fields_values[key] = value + fields_values[key] = parsed object.__setattr__(m, "__dict__", fields_values) @@ -370,6 +374,23 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object: return construct_type(value=value, type_=type_, metadata=getattr(field, "metadata", None)) +def _get_extra_fields_type(cls: type[pydantic.BaseModel]) -> type | None: + if not PYDANTIC_V2: + # TODO + return None + + schema = cls.__pydantic_core_schema__ + if schema["type"] == "model": + fields = schema["schema"] + if fields["type"] == "model-fields": + extras = fields.get("extras_schema") + if extras and "cls" in extras: + # mypy can't narrow the type + return extras["cls"] # type: ignore[no-any-return] + + return None + + def is_basemodel(type_: type) -> bool: """Returns whether or not the given type is either a `BaseModel` or a union of `BaseModel`""" if is_union(type_): diff --git a/tests/test_models.py b/tests/test_models.py index 1e7293a8..72f55a83 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,5 +1,5 @@ import json -from typing import Any, Dict, List, Union, Optional, cast +from typing import TYPE_CHECKING, Any, Dict, List, Union, Optional, cast from datetime import datetime, timezone from typing_extensions import Literal, Annotated, TypeAliasType @@ -934,3 +934,30 @@ class Type2(BaseModel): ) assert isinstance(model, Type1) assert isinstance(model.value, InnerType2) + + +@pytest.mark.skipif(not PYDANTIC_V2, reason="this is only supported in pydantic v2 for now") +def test_extra_properties() -> None: + class Item(BaseModel): + prop: int + + class Model(BaseModel): + __pydantic_extra__: Dict[str, Item] = Field(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + other: str + + if TYPE_CHECKING: + + def __getattr__(self, attr: str) -> Item: ... + + model = construct_type( + type_=Model, + value={ + "a": {"prop": 1}, + "other": "foo", + }, + ) + assert isinstance(model, Model) + assert model.a.prop == 1 + assert isinstance(model.a, Item) + assert model.other == "foo" From 75ac0967954c76142449c2d51b1d8cd211300668 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 22:49:36 +0000 Subject: [PATCH 129/448] feat(api): add action name to the response to invoke --- .stats.yml | 4 ++-- src/kernel/types/invocation_create_response.py | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 38d124b5..5c1470b3 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 19 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-84945582139b11633f792c1052a33e6af9cafc96bbafc2902a905312d14c4cc1.yml -openapi_spec_hash: c77be216626b789a543529a6de56faed +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-5e4716f7fce42bbcecc7ecb699c1c467ae778d74d558f7a260d531e2af1a7f30.yml +openapi_spec_hash: f545dcef9001b00c2604e3dcc6a12f7a config_hash: 65328ff206b8c0168c915914506d9dba diff --git a/src/kernel/types/invocation_create_response.py b/src/kernel/types/invocation_create_response.py index d58f2623..21fbcf33 100644 --- a/src/kernel/types/invocation_create_response.py +++ b/src/kernel/types/invocation_create_response.py @@ -12,6 +12,9 @@ class InvocationCreateResponse(BaseModel): id: str """ID of the invocation""" + action_name: str + """Name of the action invoked""" + status: Literal["queued", "running", "succeeded", "failed"] """Status of the invocation""" From 2b3892c49b0aad96d771a01b1ed838ae27123927 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 22:53:35 +0000 Subject: [PATCH 130/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 2b28d6ec..34dc535b 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.8.1" + ".": "0.8.2" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 83d7c720..5a5d20e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.8.1" +version = "0.8.2" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 8e3e5204..0401c33c 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.8.1" # x-release-please-version +__version__ = "0.8.2" # x-release-please-version From bda8858568b012b1274bd18f3b4a1e76dc893dc6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 25 Jul 2025 03:34:16 +0000 Subject: [PATCH 131/448] chore(project): add settings file for vscode --- .gitignore | 1 - .vscode/settings.json | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index 87797408..95ceb189 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ .prism.log -.vscode _dev __pycache__ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..5b010307 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.analysis.importFormat": "relative", +} From d9c31fc094cf2cd6359b57f15165856094d96a34 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 30 Jul 2025 00:52:47 +0000 Subject: [PATCH 132/448] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 5c1470b3..fbe1e82b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 19 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-5e4716f7fce42bbcecc7ecb699c1c467ae778d74d558f7a260d531e2af1a7f30.yml -openapi_spec_hash: f545dcef9001b00c2604e3dcc6a12f7a +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-9f2d347a4bcb03aed092ba4495aac090c3d988e9a99af091ee35c09994adad8b.yml +openapi_spec_hash: 73b92bd5503ab6c64dc26da31cca36e2 config_hash: 65328ff206b8c0168c915914506d9dba From 60c5b0e7f976fd21b94ccae4ba9ab260b9a1828e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 04:38:53 +0000 Subject: [PATCH 133/448] feat(client): support file upload requests --- src/kernel/_base_client.py | 5 ++++- src/kernel/_files.py | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/kernel/_base_client.py b/src/kernel/_base_client.py index a654874a..79cd0901 100644 --- a/src/kernel/_base_client.py +++ b/src/kernel/_base_client.py @@ -532,7 +532,10 @@ def _build_request( is_body_allowed = options.method.lower() != "get" if is_body_allowed: - kwargs["json"] = json_data if is_given(json_data) else None + if isinstance(json_data, bytes): + kwargs["content"] = json_data + else: + kwargs["json"] = json_data if is_given(json_data) else None kwargs["files"] = files else: headers.pop("Content-Type", None) diff --git a/src/kernel/_files.py b/src/kernel/_files.py index 63dab8a5..9a6dd194 100644 --- a/src/kernel/_files.py +++ b/src/kernel/_files.py @@ -69,12 +69,12 @@ def _transform_file(file: FileTypes) -> HttpxFileTypes: return file if is_tuple_t(file): - return (file[0], _read_file_content(file[1]), *file[2:]) + return (file[0], read_file_content(file[1]), *file[2:]) raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple") -def _read_file_content(file: FileContent) -> HttpxFileContent: +def read_file_content(file: FileContent) -> HttpxFileContent: if isinstance(file, os.PathLike): return pathlib.Path(file).read_bytes() return file @@ -111,12 +111,12 @@ async def _async_transform_file(file: FileTypes) -> HttpxFileTypes: return file if is_tuple_t(file): - return (file[0], await _async_read_file_content(file[1]), *file[2:]) + return (file[0], await async_read_file_content(file[1]), *file[2:]) raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple") -async def _async_read_file_content(file: FileContent) -> HttpxFileContent: +async def async_read_file_content(file: FileContent) -> HttpxFileContent: if isinstance(file, os.PathLike): return await anyio.Path(file).read_bytes() From ec9aa826cb161b5741eaf73743ce4e0586f79ee3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 1 Aug 2025 19:15:57 +0000 Subject: [PATCH 134/448] feat(api): lower default timeout to 5s --- .stats.yml | 2 +- README.md | 4 ++-- src/kernel/_constants.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.stats.yml b/.stats.yml index fbe1e82b..c8304edf 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 19 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-9f2d347a4bcb03aed092ba4495aac090c3d988e9a99af091ee35c09994adad8b.yml openapi_spec_hash: 73b92bd5503ab6c64dc26da31cca36e2 -config_hash: 65328ff206b8c0168c915914506d9dba +config_hash: aafe2b8c43d82d9838c8b77cdd59189f diff --git a/README.md b/README.md index 884d10c2..87e2b84f 100644 --- a/README.md +++ b/README.md @@ -216,7 +216,7 @@ client.with_options(max_retries=5).browsers.create( ### Timeouts -By default requests time out after 1 minute. You can configure this with a `timeout` option, +By default requests time out after 5 seconds. You can configure this with a `timeout` option, which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/timeouts/#fine-tuning-the-configuration) object: ```python @@ -224,7 +224,7 @@ from kernel import Kernel # Configure the default for all requests: client = Kernel( - # 20 seconds (default is 1 minute) + # 20 seconds (default is 5 seconds) timeout=20.0, ) diff --git a/src/kernel/_constants.py b/src/kernel/_constants.py index 6ddf2c71..50a26f79 100644 --- a/src/kernel/_constants.py +++ b/src/kernel/_constants.py @@ -5,8 +5,8 @@ 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 timeout is 5 seconds +DEFAULT_TIMEOUT = httpx.Timeout(timeout=5, connect=5.0) DEFAULT_MAX_RETRIES = 2 DEFAULT_CONNECTION_LIMITS = httpx.Limits(max_connections=100, max_keepalive_connections=20) From b029f3369361af769eb08f585dfc451c5703d3d9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 1 Aug 2025 19:25:51 +0000 Subject: [PATCH 135/448] feat(api): manual updates --- .stats.yml | 2 +- README.md | 4 ++-- src/kernel/_constants.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.stats.yml b/.stats.yml index c8304edf..fbe1e82b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 19 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-9f2d347a4bcb03aed092ba4495aac090c3d988e9a99af091ee35c09994adad8b.yml openapi_spec_hash: 73b92bd5503ab6c64dc26da31cca36e2 -config_hash: aafe2b8c43d82d9838c8b77cdd59189f +config_hash: 65328ff206b8c0168c915914506d9dba diff --git a/README.md b/README.md index 87e2b84f..884d10c2 100644 --- a/README.md +++ b/README.md @@ -216,7 +216,7 @@ client.with_options(max_retries=5).browsers.create( ### Timeouts -By default requests time out after 5 seconds. You can configure this with a `timeout` option, +By default requests time out after 1 minute. You can configure this with a `timeout` option, which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/timeouts/#fine-tuning-the-configuration) object: ```python @@ -224,7 +224,7 @@ from kernel import Kernel # Configure the default for all requests: client = Kernel( - # 20 seconds (default is 5 seconds) + # 20 seconds (default is 1 minute) timeout=20.0, ) diff --git a/src/kernel/_constants.py b/src/kernel/_constants.py index 50a26f79..6ddf2c71 100644 --- a/src/kernel/_constants.py +++ b/src/kernel/_constants.py @@ -5,8 +5,8 @@ RAW_RESPONSE_HEADER = "X-Stainless-Raw-Response" OVERRIDE_CAST_TO_HEADER = "____stainless_override_cast_to" -# default timeout is 5 seconds -DEFAULT_TIMEOUT = httpx.Timeout(timeout=5, connect=5.0) +# 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) From ee0c20bc1bdf3ad827539a1d85eb1db701b4098a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 1 Aug 2025 19:34:30 +0000 Subject: [PATCH 136/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 34dc535b..a3bdfd2f 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.8.2" + ".": "0.8.3" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 5a5d20e2..5927c84a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.8.2" +version = "0.8.3" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 0401c33c..98240d71 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.8.2" # x-release-please-version +__version__ = "0.8.3" # x-release-please-version From d8475693adf0d7c7fa3ae3df08eb6ab568ae4371 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 6 Aug 2025 05:32:21 +0000 Subject: [PATCH 137/448] chore(internal): fix ruff target version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5927c84a..a4c4b29d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -159,7 +159,7 @@ reportPrivateUsage = false [tool.ruff] line-length = 120 output-format = "grouped" -target-version = "py37" +target-version = "py38" [tool.ruff.format] docstring-code-format = true From 2d5ee17dde330b6ceb36dcf2fef7d529f53ca708 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 7 Aug 2025 21:27:53 +0000 Subject: [PATCH 138/448] feat(api): browser instance file i/o --- .stats.yml | 8 +- api.md | 34 + src/kernel/resources/browsers/__init__.py | 14 + src/kernel/resources/browsers/browsers.py | 32 + src/kernel/resources/browsers/fs/__init__.py | 33 + src/kernel/resources/browsers/fs/fs.py | 1049 +++++++++++++++++ src/kernel/resources/browsers/fs/watch.py | 369 ++++++ src/kernel/types/browsers/__init__.py | 11 + .../browsers/f_create_directory_params.py | 15 + .../browsers/f_delete_directory_params.py | 12 + .../types/browsers/f_delete_file_params.py | 12 + .../types/browsers/f_file_info_params.py | 12 + .../types/browsers/f_file_info_response.py | 27 + .../types/browsers/f_list_files_params.py | 12 + .../types/browsers/f_list_files_response.py | 32 + src/kernel/types/browsers/f_move_params.py | 15 + .../types/browsers/f_read_file_params.py | 12 + .../browsers/f_set_file_permissions_params.py | 21 + .../types/browsers/f_write_file_params.py | 15 + src/kernel/types/browsers/fs/__init__.py | 7 + .../browsers/fs/watch_events_response.py | 22 + .../types/browsers/fs/watch_start_params.py | 15 + .../types/browsers/fs/watch_start_response.py | 12 + tests/api_resources/browsers/fs/__init__.py | 1 + tests/api_resources/browsers/fs/test_watch.py | 358 ++++++ tests/api_resources/browsers/test_fs.py | 977 +++++++++++++++ 26 files changed, 3123 insertions(+), 4 deletions(-) create mode 100644 src/kernel/resources/browsers/fs/__init__.py create mode 100644 src/kernel/resources/browsers/fs/fs.py create mode 100644 src/kernel/resources/browsers/fs/watch.py create mode 100644 src/kernel/types/browsers/f_create_directory_params.py create mode 100644 src/kernel/types/browsers/f_delete_directory_params.py create mode 100644 src/kernel/types/browsers/f_delete_file_params.py create mode 100644 src/kernel/types/browsers/f_file_info_params.py create mode 100644 src/kernel/types/browsers/f_file_info_response.py create mode 100644 src/kernel/types/browsers/f_list_files_params.py create mode 100644 src/kernel/types/browsers/f_list_files_response.py create mode 100644 src/kernel/types/browsers/f_move_params.py create mode 100644 src/kernel/types/browsers/f_read_file_params.py create mode 100644 src/kernel/types/browsers/f_set_file_permissions_params.py create mode 100644 src/kernel/types/browsers/f_write_file_params.py create mode 100644 src/kernel/types/browsers/fs/__init__.py create mode 100644 src/kernel/types/browsers/fs/watch_events_response.py create mode 100644 src/kernel/types/browsers/fs/watch_start_params.py create mode 100644 src/kernel/types/browsers/fs/watch_start_response.py create mode 100644 tests/api_resources/browsers/fs/__init__.py create mode 100644 tests/api_resources/browsers/fs/test_watch.py create mode 100644 tests/api_resources/browsers/test_fs.py diff --git a/.stats.yml b/.stats.yml index fbe1e82b..062204b4 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 19 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-9f2d347a4bcb03aed092ba4495aac090c3d988e9a99af091ee35c09994adad8b.yml -openapi_spec_hash: 73b92bd5503ab6c64dc26da31cca36e2 -config_hash: 65328ff206b8c0168c915914506d9dba +configured_endpoints: 31 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-e907afeabfeea49dedd783112ac3fd29267bc86f3d594f89ba9a2abf2bcbc9d8.yml +openapi_spec_hash: 060ca6288c1a09b6d1bdf207a0011165 +config_hash: f67e4b33b2fb30c1405ee2fff8096320 diff --git a/api.md b/api.md index 434ace20..f03855dc 100644 --- a/api.md +++ b/api.md @@ -94,3 +94,37 @@ Methods: - client.browsers.replays.download(replay_id, \*, id) -> BinaryAPIResponse - client.browsers.replays.start(id, \*\*params) -> ReplayStartResponse - client.browsers.replays.stop(replay_id, \*, id) -> None + +## Fs + +Types: + +```python +from kernel.types.browsers import FFileInfoResponse, FListFilesResponse +``` + +Methods: + +- client.browsers.fs.create_directory(id, \*\*params) -> None +- client.browsers.fs.delete_directory(id, \*\*params) -> None +- client.browsers.fs.delete_file(id, \*\*params) -> None +- client.browsers.fs.file_info(id, \*\*params) -> FFileInfoResponse +- client.browsers.fs.list_files(id, \*\*params) -> FListFilesResponse +- client.browsers.fs.move(id, \*\*params) -> None +- client.browsers.fs.read_file(id, \*\*params) -> BinaryAPIResponse +- client.browsers.fs.set_file_permissions(id, \*\*params) -> None +- client.browsers.fs.write_file(id, contents, \*\*params) -> None + +### Watch + +Types: + +```python +from kernel.types.browsers.fs import WatchEventsResponse, WatchStartResponse +``` + +Methods: + +- client.browsers.fs.watch.events(watch_id, \*, id) -> WatchEventsResponse +- client.browsers.fs.watch.start(id, \*\*params) -> WatchStartResponse +- client.browsers.fs.watch.stop(watch_id, \*, id) -> None diff --git a/src/kernel/resources/browsers/__init__.py b/src/kernel/resources/browsers/__init__.py index 236b5f71..41452e9d 100644 --- a/src/kernel/resources/browsers/__init__.py +++ b/src/kernel/resources/browsers/__init__.py @@ -1,5 +1,13 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from .fs import ( + FsResource, + AsyncFsResource, + FsResourceWithRawResponse, + AsyncFsResourceWithRawResponse, + FsResourceWithStreamingResponse, + AsyncFsResourceWithStreamingResponse, +) from .replays import ( ReplaysResource, AsyncReplaysResource, @@ -24,6 +32,12 @@ "AsyncReplaysResourceWithRawResponse", "ReplaysResourceWithStreamingResponse", "AsyncReplaysResourceWithStreamingResponse", + "FsResource", + "AsyncFsResource", + "FsResourceWithRawResponse", + "AsyncFsResourceWithRawResponse", + "FsResourceWithStreamingResponse", + "AsyncFsResourceWithStreamingResponse", "BrowsersResource", "AsyncBrowsersResource", "BrowsersResourceWithRawResponse", diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index b44573e5..6d29c9e9 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -4,6 +4,14 @@ import httpx +from .fs.fs import ( + FsResource, + AsyncFsResource, + FsResourceWithRawResponse, + AsyncFsResourceWithRawResponse, + FsResourceWithStreamingResponse, + AsyncFsResourceWithStreamingResponse, +) from ...types import browser_create_params, browser_delete_params from .replays import ( ReplaysResource, @@ -37,6 +45,10 @@ class BrowsersResource(SyncAPIResource): def replays(self) -> ReplaysResource: return ReplaysResource(self._client) + @cached_property + def fs(self) -> FsResource: + return FsResource(self._client) + @cached_property def with_raw_response(self) -> BrowsersResourceWithRawResponse: """ @@ -239,6 +251,10 @@ class AsyncBrowsersResource(AsyncAPIResource): def replays(self) -> AsyncReplaysResource: return AsyncReplaysResource(self._client) + @cached_property + def fs(self) -> AsyncFsResource: + return AsyncFsResource(self._client) + @cached_property def with_raw_response(self) -> AsyncBrowsersResourceWithRawResponse: """ @@ -462,6 +478,10 @@ def __init__(self, browsers: BrowsersResource) -> None: def replays(self) -> ReplaysResourceWithRawResponse: return ReplaysResourceWithRawResponse(self._browsers.replays) + @cached_property + def fs(self) -> FsResourceWithRawResponse: + return FsResourceWithRawResponse(self._browsers.fs) + class AsyncBrowsersResourceWithRawResponse: def __init__(self, browsers: AsyncBrowsersResource) -> None: @@ -487,6 +507,10 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None: def replays(self) -> AsyncReplaysResourceWithRawResponse: return AsyncReplaysResourceWithRawResponse(self._browsers.replays) + @cached_property + def fs(self) -> AsyncFsResourceWithRawResponse: + return AsyncFsResourceWithRawResponse(self._browsers.fs) + class BrowsersResourceWithStreamingResponse: def __init__(self, browsers: BrowsersResource) -> None: @@ -512,6 +536,10 @@ def __init__(self, browsers: BrowsersResource) -> None: def replays(self) -> ReplaysResourceWithStreamingResponse: return ReplaysResourceWithStreamingResponse(self._browsers.replays) + @cached_property + def fs(self) -> FsResourceWithStreamingResponse: + return FsResourceWithStreamingResponse(self._browsers.fs) + class AsyncBrowsersResourceWithStreamingResponse: def __init__(self, browsers: AsyncBrowsersResource) -> None: @@ -536,3 +564,7 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None: @cached_property def replays(self) -> AsyncReplaysResourceWithStreamingResponse: return AsyncReplaysResourceWithStreamingResponse(self._browsers.replays) + + @cached_property + def fs(self) -> AsyncFsResourceWithStreamingResponse: + return AsyncFsResourceWithStreamingResponse(self._browsers.fs) diff --git a/src/kernel/resources/browsers/fs/__init__.py b/src/kernel/resources/browsers/fs/__init__.py new file mode 100644 index 00000000..8195b3f9 --- /dev/null +++ b/src/kernel/resources/browsers/fs/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .fs import ( + FsResource, + AsyncFsResource, + FsResourceWithRawResponse, + AsyncFsResourceWithRawResponse, + FsResourceWithStreamingResponse, + AsyncFsResourceWithStreamingResponse, +) +from .watch import ( + WatchResource, + AsyncWatchResource, + WatchResourceWithRawResponse, + AsyncWatchResourceWithRawResponse, + WatchResourceWithStreamingResponse, + AsyncWatchResourceWithStreamingResponse, +) + +__all__ = [ + "WatchResource", + "AsyncWatchResource", + "WatchResourceWithRawResponse", + "AsyncWatchResourceWithRawResponse", + "WatchResourceWithStreamingResponse", + "AsyncWatchResourceWithStreamingResponse", + "FsResource", + "AsyncFsResource", + "FsResourceWithRawResponse", + "AsyncFsResourceWithRawResponse", + "FsResourceWithStreamingResponse", + "AsyncFsResourceWithStreamingResponse", +] diff --git a/src/kernel/resources/browsers/fs/fs.py b/src/kernel/resources/browsers/fs/fs.py new file mode 100644 index 00000000..3563c7cb --- /dev/null +++ b/src/kernel/resources/browsers/fs/fs.py @@ -0,0 +1,1049 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from .watch import ( + WatchResource, + AsyncWatchResource, + WatchResourceWithRawResponse, + AsyncWatchResourceWithRawResponse, + WatchResourceWithStreamingResponse, + AsyncWatchResourceWithStreamingResponse, +) +from ...._files import read_file_content, async_read_file_content +from ...._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven, FileContent +from ...._utils import maybe_transform, async_maybe_transform +from ...._compat import cached_property +from ...._resource import SyncAPIResource, AsyncAPIResource +from ...._response import ( + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + to_custom_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_custom_streamed_response_wrapper, + async_to_custom_raw_response_wrapper, + async_to_custom_streamed_response_wrapper, +) +from ...._base_client import make_request_options +from ....types.browsers import ( + f_move_params, + f_file_info_params, + f_read_file_params, + f_list_files_params, + f_write_file_params, + f_delete_file_params, + f_create_directory_params, + f_delete_directory_params, + f_set_file_permissions_params, +) +from ....types.browsers.f_file_info_response import FFileInfoResponse +from ....types.browsers.f_list_files_response import FListFilesResponse + +__all__ = ["FsResource", "AsyncFsResource"] + + +class FsResource(SyncAPIResource): + @cached_property + def watch(self) -> WatchResource: + return WatchResource(self._client) + + @cached_property + def with_raw_response(self) -> FsResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return FsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> FsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return FsResourceWithStreamingResponse(self) + + def create_directory( + self, + id: str, + *, + path: str, + mode: 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, + ) -> None: + """ + Create a new directory + + Args: + path: Absolute directory path to create. + + mode: Optional directory mode (octal string, e.g. 755). Defaults to 755. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._put( + f"/browsers/{id}/fs/create_directory", + body=maybe_transform( + { + "path": path, + "mode": mode, + }, + f_create_directory_params.FCreateDirectoryParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + def delete_directory( + self, + id: str, + *, + path: 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 directory + + Args: + path: Absolute path to delete. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._put( + f"/browsers/{id}/fs/delete_directory", + body=maybe_transform({"path": path}, f_delete_directory_params.FDeleteDirectoryParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + def delete_file( + self, + id: str, + *, + path: 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 file + + Args: + path: Absolute path to delete. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._put( + f"/browsers/{id}/fs/delete_file", + body=maybe_transform({"path": path}, f_delete_file_params.FDeleteFileParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + def file_info( + self, + id: str, + *, + path: 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, + ) -> FFileInfoResponse: + """ + Get information about a file or directory + + Args: + path: Absolute path of the file or directory. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/browsers/{id}/fs/file_info", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform({"path": path}, f_file_info_params.FFileInfoParams), + ), + cast_to=FFileInfoResponse, + ) + + def list_files( + self, + id: str, + *, + path: 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, + ) -> FListFilesResponse: + """ + List files in a directory + + Args: + path: Absolute directory path. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/browsers/{id}/fs/list_files", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform({"path": path}, f_list_files_params.FListFilesParams), + ), + cast_to=FListFilesResponse, + ) + + def move( + self, + id: str, + *, + dest_path: str, + src_path: 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: + """ + Move or rename a file or directory + + Args: + dest_path: Absolute destination path. + + src_path: Absolute source path. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._put( + f"/browsers/{id}/fs/move", + body=maybe_transform( + { + "dest_path": dest_path, + "src_path": src_path, + }, + f_move_params.FMoveParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + def read_file( + self, + id: str, + *, + path: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BinaryAPIResponse: + """ + Read file contents + + Args: + path: Absolute file path to read. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + return self._get( + f"/browsers/{id}/fs/read_file", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform({"path": path}, f_read_file_params.FReadFileParams), + ), + cast_to=BinaryAPIResponse, + ) + + def set_file_permissions( + self, + id: str, + *, + mode: str, + path: str, + group: str | NotGiven = NOT_GIVEN, + owner: 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, + ) -> None: + """ + Set file or directory permissions/ownership + + Args: + mode: File mode bits (octal string, e.g. 644). + + path: Absolute path whose permissions are to be changed. + + group: New group name or GID. + + owner: New owner username or UID. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._put( + f"/browsers/{id}/fs/set_file_permissions", + body=maybe_transform( + { + "mode": mode, + "path": path, + "group": group, + "owner": owner, + }, + f_set_file_permissions_params.FSetFilePermissionsParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + def write_file( + self, + id: str, + contents: FileContent, + *, + path: str, + mode: 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, + ) -> None: + """ + Write or create a file + + Args: + path: Destination absolute file path. + + mode: Optional file mode (octal string, e.g. 644). Defaults to 644. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + extra_headers["Content-Type"] = "application/octet-stream" + return self._put( + f"/browsers/{id}/fs/write_file", + body=read_file_content(contents), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "path": path, + "mode": mode, + }, + f_write_file_params.FWriteFileParams, + ), + ), + cast_to=NoneType, + ) + + +class AsyncFsResource(AsyncAPIResource): + @cached_property + def watch(self) -> AsyncWatchResource: + return AsyncWatchResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncFsResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncFsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncFsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return AsyncFsResourceWithStreamingResponse(self) + + async def create_directory( + self, + id: str, + *, + path: str, + mode: 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, + ) -> None: + """ + Create a new directory + + Args: + path: Absolute directory path to create. + + mode: Optional directory mode (octal string, e.g. 755). Defaults to 755. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._put( + f"/browsers/{id}/fs/create_directory", + body=await async_maybe_transform( + { + "path": path, + "mode": mode, + }, + f_create_directory_params.FCreateDirectoryParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + async def delete_directory( + self, + id: str, + *, + path: 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 directory + + Args: + path: Absolute path to delete. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._put( + f"/browsers/{id}/fs/delete_directory", + body=await async_maybe_transform({"path": path}, f_delete_directory_params.FDeleteDirectoryParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + async def delete_file( + self, + id: str, + *, + path: 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 file + + Args: + path: Absolute path to delete. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._put( + f"/browsers/{id}/fs/delete_file", + body=await async_maybe_transform({"path": path}, f_delete_file_params.FDeleteFileParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + async def file_info( + self, + id: str, + *, + path: 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, + ) -> FFileInfoResponse: + """ + Get information about a file or directory + + Args: + path: Absolute path of the file or directory. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/browsers/{id}/fs/file_info", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform({"path": path}, f_file_info_params.FFileInfoParams), + ), + cast_to=FFileInfoResponse, + ) + + async def list_files( + self, + id: str, + *, + path: 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, + ) -> FListFilesResponse: + """ + List files in a directory + + Args: + path: Absolute directory path. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/browsers/{id}/fs/list_files", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform({"path": path}, f_list_files_params.FListFilesParams), + ), + cast_to=FListFilesResponse, + ) + + async def move( + self, + id: str, + *, + dest_path: str, + src_path: 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: + """ + Move or rename a file or directory + + Args: + dest_path: Absolute destination path. + + src_path: Absolute source path. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._put( + f"/browsers/{id}/fs/move", + body=await async_maybe_transform( + { + "dest_path": dest_path, + "src_path": src_path, + }, + f_move_params.FMoveParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + async def read_file( + self, + id: str, + *, + path: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncBinaryAPIResponse: + """ + Read file contents + + Args: + path: Absolute file path to read. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + return await self._get( + f"/browsers/{id}/fs/read_file", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform({"path": path}, f_read_file_params.FReadFileParams), + ), + cast_to=AsyncBinaryAPIResponse, + ) + + async def set_file_permissions( + self, + id: str, + *, + mode: str, + path: str, + group: str | NotGiven = NOT_GIVEN, + owner: 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, + ) -> None: + """ + Set file or directory permissions/ownership + + Args: + mode: File mode bits (octal string, e.g. 644). + + path: Absolute path whose permissions are to be changed. + + group: New group name or GID. + + owner: New owner username or UID. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._put( + f"/browsers/{id}/fs/set_file_permissions", + body=await async_maybe_transform( + { + "mode": mode, + "path": path, + "group": group, + "owner": owner, + }, + f_set_file_permissions_params.FSetFilePermissionsParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + async def write_file( + self, + id: str, + contents: FileContent, + *, + path: str, + mode: 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, + ) -> None: + """ + Write or create a file + + Args: + path: Destination absolute file path. + + mode: Optional file mode (octal string, e.g. 644). Defaults to 644. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + extra_headers["Content-Type"] = "application/octet-stream" + return await self._put( + f"/browsers/{id}/fs/write_file", + body=await async_read_file_content(contents), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "path": path, + "mode": mode, + }, + f_write_file_params.FWriteFileParams, + ), + ), + cast_to=NoneType, + ) + + +class FsResourceWithRawResponse: + def __init__(self, fs: FsResource) -> None: + self._fs = fs + + self.create_directory = to_raw_response_wrapper( + fs.create_directory, + ) + self.delete_directory = to_raw_response_wrapper( + fs.delete_directory, + ) + self.delete_file = to_raw_response_wrapper( + fs.delete_file, + ) + self.file_info = to_raw_response_wrapper( + fs.file_info, + ) + self.list_files = to_raw_response_wrapper( + fs.list_files, + ) + self.move = to_raw_response_wrapper( + fs.move, + ) + self.read_file = to_custom_raw_response_wrapper( + fs.read_file, + BinaryAPIResponse, + ) + self.set_file_permissions = to_raw_response_wrapper( + fs.set_file_permissions, + ) + self.write_file = to_raw_response_wrapper( + fs.write_file, + ) + + @cached_property + def watch(self) -> WatchResourceWithRawResponse: + return WatchResourceWithRawResponse(self._fs.watch) + + +class AsyncFsResourceWithRawResponse: + def __init__(self, fs: AsyncFsResource) -> None: + self._fs = fs + + self.create_directory = async_to_raw_response_wrapper( + fs.create_directory, + ) + self.delete_directory = async_to_raw_response_wrapper( + fs.delete_directory, + ) + self.delete_file = async_to_raw_response_wrapper( + fs.delete_file, + ) + self.file_info = async_to_raw_response_wrapper( + fs.file_info, + ) + self.list_files = async_to_raw_response_wrapper( + fs.list_files, + ) + self.move = async_to_raw_response_wrapper( + fs.move, + ) + self.read_file = async_to_custom_raw_response_wrapper( + fs.read_file, + AsyncBinaryAPIResponse, + ) + self.set_file_permissions = async_to_raw_response_wrapper( + fs.set_file_permissions, + ) + self.write_file = async_to_raw_response_wrapper( + fs.write_file, + ) + + @cached_property + def watch(self) -> AsyncWatchResourceWithRawResponse: + return AsyncWatchResourceWithRawResponse(self._fs.watch) + + +class FsResourceWithStreamingResponse: + def __init__(self, fs: FsResource) -> None: + self._fs = fs + + self.create_directory = to_streamed_response_wrapper( + fs.create_directory, + ) + self.delete_directory = to_streamed_response_wrapper( + fs.delete_directory, + ) + self.delete_file = to_streamed_response_wrapper( + fs.delete_file, + ) + self.file_info = to_streamed_response_wrapper( + fs.file_info, + ) + self.list_files = to_streamed_response_wrapper( + fs.list_files, + ) + self.move = to_streamed_response_wrapper( + fs.move, + ) + self.read_file = to_custom_streamed_response_wrapper( + fs.read_file, + StreamedBinaryAPIResponse, + ) + self.set_file_permissions = to_streamed_response_wrapper( + fs.set_file_permissions, + ) + self.write_file = to_streamed_response_wrapper( + fs.write_file, + ) + + @cached_property + def watch(self) -> WatchResourceWithStreamingResponse: + return WatchResourceWithStreamingResponse(self._fs.watch) + + +class AsyncFsResourceWithStreamingResponse: + def __init__(self, fs: AsyncFsResource) -> None: + self._fs = fs + + self.create_directory = async_to_streamed_response_wrapper( + fs.create_directory, + ) + self.delete_directory = async_to_streamed_response_wrapper( + fs.delete_directory, + ) + self.delete_file = async_to_streamed_response_wrapper( + fs.delete_file, + ) + self.file_info = async_to_streamed_response_wrapper( + fs.file_info, + ) + self.list_files = async_to_streamed_response_wrapper( + fs.list_files, + ) + self.move = async_to_streamed_response_wrapper( + fs.move, + ) + self.read_file = async_to_custom_streamed_response_wrapper( + fs.read_file, + AsyncStreamedBinaryAPIResponse, + ) + self.set_file_permissions = async_to_streamed_response_wrapper( + fs.set_file_permissions, + ) + self.write_file = async_to_streamed_response_wrapper( + fs.write_file, + ) + + @cached_property + def watch(self) -> AsyncWatchResourceWithStreamingResponse: + return AsyncWatchResourceWithStreamingResponse(self._fs.watch) diff --git a/src/kernel/resources/browsers/fs/watch.py b/src/kernel/resources/browsers/fs/watch.py new file mode 100644 index 00000000..a35e0def --- /dev/null +++ b/src/kernel/resources/browsers/fs/watch.py @@ -0,0 +1,369 @@ +# 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 ...._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 ...._streaming import Stream, AsyncStream +from ...._base_client import make_request_options +from ....types.browsers.fs import watch_start_params +from ....types.browsers.fs.watch_start_response import WatchStartResponse +from ....types.browsers.fs.watch_events_response import WatchEventsResponse + +__all__ = ["WatchResource", "AsyncWatchResource"] + + +class WatchResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> WatchResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return WatchResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> WatchResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return WatchResourceWithStreamingResponse(self) + + def events( + self, + watch_id: str, + *, + id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Stream[WatchEventsResponse]: + """ + Stream filesystem events for a watch + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not watch_id: + raise ValueError(f"Expected a non-empty value for `watch_id` but received {watch_id!r}") + extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} + return self._get( + f"/browsers/{id}/fs/watch/{watch_id}/events", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=WatchEventsResponse, + stream=True, + stream_cls=Stream[WatchEventsResponse], + ) + + def start( + self, + id: str, + *, + path: str, + recursive: 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, + ) -> WatchStartResponse: + """ + Watch a directory for changes + + Args: + path: Directory to watch. + + recursive: Whether to watch recursively. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/browsers/{id}/fs/watch", + body=maybe_transform( + { + "path": path, + "recursive": recursive, + }, + watch_start_params.WatchStartParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=WatchStartResponse, + ) + + def stop( + self, + watch_id: str, + *, + id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """ + Stop watching a directory + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not watch_id: + raise ValueError(f"Expected a non-empty value for `watch_id` but received {watch_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + f"/browsers/{id}/fs/watch/{watch_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class AsyncWatchResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncWatchResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncWatchResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncWatchResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return AsyncWatchResourceWithStreamingResponse(self) + + async def events( + self, + watch_id: str, + *, + id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncStream[WatchEventsResponse]: + """ + Stream filesystem events for a watch + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not watch_id: + raise ValueError(f"Expected a non-empty value for `watch_id` but received {watch_id!r}") + extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} + return await self._get( + f"/browsers/{id}/fs/watch/{watch_id}/events", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=WatchEventsResponse, + stream=True, + stream_cls=AsyncStream[WatchEventsResponse], + ) + + async def start( + self, + id: str, + *, + path: str, + recursive: 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, + ) -> WatchStartResponse: + """ + Watch a directory for changes + + Args: + path: Directory to watch. + + recursive: Whether to watch recursively. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/browsers/{id}/fs/watch", + body=await async_maybe_transform( + { + "path": path, + "recursive": recursive, + }, + watch_start_params.WatchStartParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=WatchStartResponse, + ) + + async def stop( + self, + watch_id: str, + *, + id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """ + Stop watching a directory + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not watch_id: + raise ValueError(f"Expected a non-empty value for `watch_id` but received {watch_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + f"/browsers/{id}/fs/watch/{watch_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class WatchResourceWithRawResponse: + def __init__(self, watch: WatchResource) -> None: + self._watch = watch + + self.events = to_raw_response_wrapper( + watch.events, + ) + self.start = to_raw_response_wrapper( + watch.start, + ) + self.stop = to_raw_response_wrapper( + watch.stop, + ) + + +class AsyncWatchResourceWithRawResponse: + def __init__(self, watch: AsyncWatchResource) -> None: + self._watch = watch + + self.events = async_to_raw_response_wrapper( + watch.events, + ) + self.start = async_to_raw_response_wrapper( + watch.start, + ) + self.stop = async_to_raw_response_wrapper( + watch.stop, + ) + + +class WatchResourceWithStreamingResponse: + def __init__(self, watch: WatchResource) -> None: + self._watch = watch + + self.events = to_streamed_response_wrapper( + watch.events, + ) + self.start = to_streamed_response_wrapper( + watch.start, + ) + self.stop = to_streamed_response_wrapper( + watch.stop, + ) + + +class AsyncWatchResourceWithStreamingResponse: + def __init__(self, watch: AsyncWatchResource) -> None: + self._watch = watch + + self.events = async_to_streamed_response_wrapper( + watch.events, + ) + self.start = async_to_streamed_response_wrapper( + watch.start, + ) + self.stop = async_to_streamed_response_wrapper( + watch.stop, + ) diff --git a/src/kernel/types/browsers/__init__.py b/src/kernel/types/browsers/__init__.py index 61b18bc3..c4e80a89 100644 --- a/src/kernel/types/browsers/__init__.py +++ b/src/kernel/types/browsers/__init__.py @@ -2,6 +2,17 @@ from __future__ import annotations +from .f_move_params import FMoveParams as FMoveParams +from .f_file_info_params import FFileInfoParams as FFileInfoParams +from .f_read_file_params import FReadFileParams as FReadFileParams +from .f_list_files_params import FListFilesParams as FListFilesParams +from .f_write_file_params import FWriteFileParams as FWriteFileParams from .replay_start_params import ReplayStartParams as ReplayStartParams +from .f_delete_file_params import FDeleteFileParams as FDeleteFileParams +from .f_file_info_response import FFileInfoResponse as FFileInfoResponse from .replay_list_response import ReplayListResponse as ReplayListResponse +from .f_list_files_response import FListFilesResponse as FListFilesResponse from .replay_start_response import ReplayStartResponse as ReplayStartResponse +from .f_create_directory_params import FCreateDirectoryParams as FCreateDirectoryParams +from .f_delete_directory_params import FDeleteDirectoryParams as FDeleteDirectoryParams +from .f_set_file_permissions_params import FSetFilePermissionsParams as FSetFilePermissionsParams diff --git a/src/kernel/types/browsers/f_create_directory_params.py b/src/kernel/types/browsers/f_create_directory_params.py new file mode 100644 index 00000000..20924f38 --- /dev/null +++ b/src/kernel/types/browsers/f_create_directory_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["FCreateDirectoryParams"] + + +class FCreateDirectoryParams(TypedDict, total=False): + path: Required[str] + """Absolute directory path to create.""" + + mode: str + """Optional directory mode (octal string, e.g. 755). Defaults to 755.""" diff --git a/src/kernel/types/browsers/f_delete_directory_params.py b/src/kernel/types/browsers/f_delete_directory_params.py new file mode 100644 index 00000000..8f5a0863 --- /dev/null +++ b/src/kernel/types/browsers/f_delete_directory_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["FDeleteDirectoryParams"] + + +class FDeleteDirectoryParams(TypedDict, total=False): + path: Required[str] + """Absolute path to delete.""" diff --git a/src/kernel/types/browsers/f_delete_file_params.py b/src/kernel/types/browsers/f_delete_file_params.py new file mode 100644 index 00000000..d79bb8a7 --- /dev/null +++ b/src/kernel/types/browsers/f_delete_file_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["FDeleteFileParams"] + + +class FDeleteFileParams(TypedDict, total=False): + path: Required[str] + """Absolute path to delete.""" diff --git a/src/kernel/types/browsers/f_file_info_params.py b/src/kernel/types/browsers/f_file_info_params.py new file mode 100644 index 00000000..9ddf41e5 --- /dev/null +++ b/src/kernel/types/browsers/f_file_info_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["FFileInfoParams"] + + +class FFileInfoParams(TypedDict, total=False): + path: Required[str] + """Absolute path of the file or directory.""" diff --git a/src/kernel/types/browsers/f_file_info_response.py b/src/kernel/types/browsers/f_file_info_response.py new file mode 100644 index 00000000..7da15740 --- /dev/null +++ b/src/kernel/types/browsers/f_file_info_response.py @@ -0,0 +1,27 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from datetime import datetime + +from ..._models import BaseModel + +__all__ = ["FFileInfoResponse"] + + +class FFileInfoResponse(BaseModel): + is_dir: bool + """Whether the path is a directory.""" + + mod_time: datetime + """Last modification time.""" + + mode: str + """File mode bits (e.g., "drwxr-xr-x" or "-rw-r--r--").""" + + name: str + """Base name of the file or directory.""" + + path: str + """Absolute path.""" + + size_bytes: int + """Size in bytes. 0 for directories.""" diff --git a/src/kernel/types/browsers/f_list_files_params.py b/src/kernel/types/browsers/f_list_files_params.py new file mode 100644 index 00000000..87026f50 --- /dev/null +++ b/src/kernel/types/browsers/f_list_files_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["FListFilesParams"] + + +class FListFilesParams(TypedDict, total=False): + path: Required[str] + """Absolute directory path.""" diff --git a/src/kernel/types/browsers/f_list_files_response.py b/src/kernel/types/browsers/f_list_files_response.py new file mode 100644 index 00000000..9fca14bd --- /dev/null +++ b/src/kernel/types/browsers/f_list_files_response.py @@ -0,0 +1,32 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List +from datetime import datetime +from typing_extensions import TypeAlias + +from ..._models import BaseModel + +__all__ = ["FListFilesResponse", "FListFilesResponseItem"] + + +class FListFilesResponseItem(BaseModel): + is_dir: bool + """Whether the path is a directory.""" + + mod_time: datetime + """Last modification time.""" + + mode: str + """File mode bits (e.g., "drwxr-xr-x" or "-rw-r--r--").""" + + name: str + """Base name of the file or directory.""" + + path: str + """Absolute path.""" + + size_bytes: int + """Size in bytes. 0 for directories.""" + + +FListFilesResponse: TypeAlias = List[FListFilesResponseItem] diff --git a/src/kernel/types/browsers/f_move_params.py b/src/kernel/types/browsers/f_move_params.py new file mode 100644 index 00000000..d324cc90 --- /dev/null +++ b/src/kernel/types/browsers/f_move_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["FMoveParams"] + + +class FMoveParams(TypedDict, total=False): + dest_path: Required[str] + """Absolute destination path.""" + + src_path: Required[str] + """Absolute source path.""" diff --git a/src/kernel/types/browsers/f_read_file_params.py b/src/kernel/types/browsers/f_read_file_params.py new file mode 100644 index 00000000..ee5d2e9e --- /dev/null +++ b/src/kernel/types/browsers/f_read_file_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["FReadFileParams"] + + +class FReadFileParams(TypedDict, total=False): + path: Required[str] + """Absolute file path to read.""" diff --git a/src/kernel/types/browsers/f_set_file_permissions_params.py b/src/kernel/types/browsers/f_set_file_permissions_params.py new file mode 100644 index 00000000..5a02c1e5 --- /dev/null +++ b/src/kernel/types/browsers/f_set_file_permissions_params.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["FSetFilePermissionsParams"] + + +class FSetFilePermissionsParams(TypedDict, total=False): + mode: Required[str] + """File mode bits (octal string, e.g. 644).""" + + path: Required[str] + """Absolute path whose permissions are to be changed.""" + + group: str + """New group name or GID.""" + + owner: str + """New owner username or UID.""" diff --git a/src/kernel/types/browsers/f_write_file_params.py b/src/kernel/types/browsers/f_write_file_params.py new file mode 100644 index 00000000..557eac10 --- /dev/null +++ b/src/kernel/types/browsers/f_write_file_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["FWriteFileParams"] + + +class FWriteFileParams(TypedDict, total=False): + path: Required[str] + """Destination absolute file path.""" + + mode: str + """Optional file mode (octal string, e.g. 644). Defaults to 644.""" diff --git a/src/kernel/types/browsers/fs/__init__.py b/src/kernel/types/browsers/fs/__init__.py new file mode 100644 index 00000000..ebd13d99 --- /dev/null +++ b/src/kernel/types/browsers/fs/__init__.py @@ -0,0 +1,7 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .watch_start_params import WatchStartParams as WatchStartParams +from .watch_start_response import WatchStartResponse as WatchStartResponse +from .watch_events_response import WatchEventsResponse as WatchEventsResponse diff --git a/src/kernel/types/browsers/fs/watch_events_response.py b/src/kernel/types/browsers/fs/watch_events_response.py new file mode 100644 index 00000000..8df2f501 --- /dev/null +++ b/src/kernel/types/browsers/fs/watch_events_response.py @@ -0,0 +1,22 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ...._models import BaseModel + +__all__ = ["WatchEventsResponse"] + + +class WatchEventsResponse(BaseModel): + path: str + """Absolute path of the file or directory.""" + + type: Literal["CREATE", "WRITE", "DELETE", "RENAME"] + """Event type.""" + + is_dir: Optional[bool] = None + """Whether the affected path is a directory.""" + + name: Optional[str] = None + """Base name of the file or directory affected.""" diff --git a/src/kernel/types/browsers/fs/watch_start_params.py b/src/kernel/types/browsers/fs/watch_start_params.py new file mode 100644 index 00000000..5afddb1d --- /dev/null +++ b/src/kernel/types/browsers/fs/watch_start_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["WatchStartParams"] + + +class WatchStartParams(TypedDict, total=False): + path: Required[str] + """Directory to watch.""" + + recursive: bool + """Whether to watch recursively.""" diff --git a/src/kernel/types/browsers/fs/watch_start_response.py b/src/kernel/types/browsers/fs/watch_start_response.py new file mode 100644 index 00000000..b9f78e49 --- /dev/null +++ b/src/kernel/types/browsers/fs/watch_start_response.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ...._models import BaseModel + +__all__ = ["WatchStartResponse"] + + +class WatchStartResponse(BaseModel): + watch_id: Optional[str] = None + """Unique identifier for the directory watch""" diff --git a/tests/api_resources/browsers/fs/__init__.py b/tests/api_resources/browsers/fs/__init__.py new file mode 100644 index 00000000..fd8019a9 --- /dev/null +++ b/tests/api_resources/browsers/fs/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/browsers/fs/test_watch.py b/tests/api_resources/browsers/fs/test_watch.py new file mode 100644 index 00000000..b815c8a5 --- /dev/null +++ b/tests/api_resources/browsers/fs/test_watch.py @@ -0,0 +1,358 @@ +# 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 kernel import Kernel, AsyncKernel +from tests.utils import assert_matches_type +from kernel.types.browsers.fs import WatchStartResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestWatch: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + def test_method_events(self, client: Kernel) -> None: + watch_stream = client.browsers.fs.watch.events( + watch_id="watch_id", + id="id", + ) + watch_stream.response.close() + + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + def test_raw_response_events(self, client: Kernel) -> None: + response = client.browsers.fs.watch.with_raw_response.events( + watch_id="watch_id", + id="id", + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = response.parse() + stream.close() + + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + def test_streaming_response_events(self, client: Kernel) -> None: + with client.browsers.fs.watch.with_streaming_response.events( + watch_id="watch_id", + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = response.parse() + stream.close() + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + def test_path_params_events(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.fs.watch.with_raw_response.events( + watch_id="watch_id", + id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `watch_id` but received ''"): + client.browsers.fs.watch.with_raw_response.events( + watch_id="", + id="id", + ) + + @pytest.mark.skip() + @parametrize + def test_method_start(self, client: Kernel) -> None: + watch = client.browsers.fs.watch.start( + id="id", + path="path", + ) + assert_matches_type(WatchStartResponse, watch, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_method_start_with_all_params(self, client: Kernel) -> None: + watch = client.browsers.fs.watch.start( + id="id", + path="path", + recursive=True, + ) + assert_matches_type(WatchStartResponse, watch, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_raw_response_start(self, client: Kernel) -> None: + response = client.browsers.fs.watch.with_raw_response.start( + id="id", + path="path", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + watch = response.parse() + assert_matches_type(WatchStartResponse, watch, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_streaming_response_start(self, client: Kernel) -> None: + with client.browsers.fs.watch.with_streaming_response.start( + id="id", + path="path", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + watch = response.parse() + assert_matches_type(WatchStartResponse, watch, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + def test_path_params_start(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.fs.watch.with_raw_response.start( + id="", + path="path", + ) + + @pytest.mark.skip() + @parametrize + def test_method_stop(self, client: Kernel) -> None: + watch = client.browsers.fs.watch.stop( + watch_id="watch_id", + id="id", + ) + assert watch is None + + @pytest.mark.skip() + @parametrize + def test_raw_response_stop(self, client: Kernel) -> None: + response = client.browsers.fs.watch.with_raw_response.stop( + watch_id="watch_id", + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + watch = response.parse() + assert watch is None + + @pytest.mark.skip() + @parametrize + def test_streaming_response_stop(self, client: Kernel) -> None: + with client.browsers.fs.watch.with_streaming_response.stop( + watch_id="watch_id", + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + watch = response.parse() + assert watch is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + def test_path_params_stop(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.fs.watch.with_raw_response.stop( + watch_id="watch_id", + id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `watch_id` but received ''"): + client.browsers.fs.watch.with_raw_response.stop( + watch_id="", + id="id", + ) + + +class TestAsyncWatch: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + async def test_method_events(self, async_client: AsyncKernel) -> None: + watch_stream = await async_client.browsers.fs.watch.events( + watch_id="watch_id", + id="id", + ) + await watch_stream.response.aclose() + + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + async def test_raw_response_events(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.fs.watch.with_raw_response.events( + watch_id="watch_id", + id="id", + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = await response.parse() + await stream.close() + + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + async def test_streaming_response_events(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.fs.watch.with_streaming_response.events( + watch_id="watch_id", + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = await response.parse() + await stream.close() + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip( + reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" + ) + @parametrize + async def test_path_params_events(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.fs.watch.with_raw_response.events( + watch_id="watch_id", + id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `watch_id` but received ''"): + await async_client.browsers.fs.watch.with_raw_response.events( + watch_id="", + id="id", + ) + + @pytest.mark.skip() + @parametrize + async def test_method_start(self, async_client: AsyncKernel) -> None: + watch = await async_client.browsers.fs.watch.start( + id="id", + path="path", + ) + assert_matches_type(WatchStartResponse, watch, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_method_start_with_all_params(self, async_client: AsyncKernel) -> None: + watch = await async_client.browsers.fs.watch.start( + id="id", + path="path", + recursive=True, + ) + assert_matches_type(WatchStartResponse, watch, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_raw_response_start(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.fs.watch.with_raw_response.start( + id="id", + path="path", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + watch = await response.parse() + assert_matches_type(WatchStartResponse, watch, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_start(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.fs.watch.with_streaming_response.start( + id="id", + path="path", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + watch = await response.parse() + assert_matches_type(WatchStartResponse, watch, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + async def test_path_params_start(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.fs.watch.with_raw_response.start( + id="", + path="path", + ) + + @pytest.mark.skip() + @parametrize + async def test_method_stop(self, async_client: AsyncKernel) -> None: + watch = await async_client.browsers.fs.watch.stop( + watch_id="watch_id", + id="id", + ) + assert watch is None + + @pytest.mark.skip() + @parametrize + async def test_raw_response_stop(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.fs.watch.with_raw_response.stop( + watch_id="watch_id", + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + watch = await response.parse() + assert watch is None + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_stop(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.fs.watch.with_streaming_response.stop( + watch_id="watch_id", + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + watch = await response.parse() + assert watch is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + async def test_path_params_stop(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.fs.watch.with_raw_response.stop( + watch_id="watch_id", + id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `watch_id` but received ''"): + await async_client.browsers.fs.watch.with_raw_response.stop( + watch_id="", + id="id", + ) diff --git a/tests/api_resources/browsers/test_fs.py b/tests/api_resources/browsers/test_fs.py new file mode 100644 index 00000000..c82e09d2 --- /dev/null +++ b/tests/api_resources/browsers/test_fs.py @@ -0,0 +1,977 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import httpx +import pytest +from respx import MockRouter + +from kernel import Kernel, AsyncKernel +from tests.utils import assert_matches_type +from kernel._response import ( + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, +) +from kernel.types.browsers import ( + FFileInfoResponse, + FListFilesResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestFs: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip() + @parametrize + def test_method_create_directory(self, client: Kernel) -> None: + f = client.browsers.fs.create_directory( + id="id", + path="/J!", + ) + assert f is None + + @pytest.mark.skip() + @parametrize + def test_method_create_directory_with_all_params(self, client: Kernel) -> None: + f = client.browsers.fs.create_directory( + id="id", + path="/J!", + mode="0611", + ) + assert f is None + + @pytest.mark.skip() + @parametrize + def test_raw_response_create_directory(self, client: Kernel) -> None: + response = client.browsers.fs.with_raw_response.create_directory( + id="id", + path="/J!", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + f = response.parse() + assert f is None + + @pytest.mark.skip() + @parametrize + def test_streaming_response_create_directory(self, client: Kernel) -> None: + with client.browsers.fs.with_streaming_response.create_directory( + id="id", + path="/J!", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + f = response.parse() + assert f is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + def test_path_params_create_directory(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.fs.with_raw_response.create_directory( + id="", + path="/J!", + ) + + @pytest.mark.skip() + @parametrize + def test_method_delete_directory(self, client: Kernel) -> None: + f = client.browsers.fs.delete_directory( + id="id", + path="/J!", + ) + assert f is None + + @pytest.mark.skip() + @parametrize + def test_raw_response_delete_directory(self, client: Kernel) -> None: + response = client.browsers.fs.with_raw_response.delete_directory( + id="id", + path="/J!", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + f = response.parse() + assert f is None + + @pytest.mark.skip() + @parametrize + def test_streaming_response_delete_directory(self, client: Kernel) -> None: + with client.browsers.fs.with_streaming_response.delete_directory( + id="id", + path="/J!", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + f = response.parse() + assert f is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + def test_path_params_delete_directory(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.fs.with_raw_response.delete_directory( + id="", + path="/J!", + ) + + @pytest.mark.skip() + @parametrize + def test_method_delete_file(self, client: Kernel) -> None: + f = client.browsers.fs.delete_file( + id="id", + path="/J!", + ) + assert f is None + + @pytest.mark.skip() + @parametrize + def test_raw_response_delete_file(self, client: Kernel) -> None: + response = client.browsers.fs.with_raw_response.delete_file( + id="id", + path="/J!", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + f = response.parse() + assert f is None + + @pytest.mark.skip() + @parametrize + def test_streaming_response_delete_file(self, client: Kernel) -> None: + with client.browsers.fs.with_streaming_response.delete_file( + id="id", + path="/J!", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + f = response.parse() + assert f is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + def test_path_params_delete_file(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.fs.with_raw_response.delete_file( + id="", + path="/J!", + ) + + @pytest.mark.skip() + @parametrize + def test_method_file_info(self, client: Kernel) -> None: + f = client.browsers.fs.file_info( + id="id", + path="/J!", + ) + assert_matches_type(FFileInfoResponse, f, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_raw_response_file_info(self, client: Kernel) -> None: + response = client.browsers.fs.with_raw_response.file_info( + id="id", + path="/J!", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + f = response.parse() + assert_matches_type(FFileInfoResponse, f, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_streaming_response_file_info(self, client: Kernel) -> None: + with client.browsers.fs.with_streaming_response.file_info( + id="id", + path="/J!", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + f = response.parse() + assert_matches_type(FFileInfoResponse, f, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + def test_path_params_file_info(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.fs.with_raw_response.file_info( + id="", + path="/J!", + ) + + @pytest.mark.skip() + @parametrize + def test_method_list_files(self, client: Kernel) -> None: + f = client.browsers.fs.list_files( + id="id", + path="/J!", + ) + assert_matches_type(FListFilesResponse, f, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_raw_response_list_files(self, client: Kernel) -> None: + response = client.browsers.fs.with_raw_response.list_files( + id="id", + path="/J!", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + f = response.parse() + assert_matches_type(FListFilesResponse, f, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_streaming_response_list_files(self, client: Kernel) -> None: + with client.browsers.fs.with_streaming_response.list_files( + id="id", + path="/J!", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + f = response.parse() + assert_matches_type(FListFilesResponse, f, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + def test_path_params_list_files(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.fs.with_raw_response.list_files( + id="", + path="/J!", + ) + + @pytest.mark.skip() + @parametrize + def test_method_move(self, client: Kernel) -> None: + f = client.browsers.fs.move( + id="id", + dest_path="/J!", + src_path="/J!", + ) + assert f is None + + @pytest.mark.skip() + @parametrize + def test_raw_response_move(self, client: Kernel) -> None: + response = client.browsers.fs.with_raw_response.move( + id="id", + dest_path="/J!", + src_path="/J!", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + f = response.parse() + assert f is None + + @pytest.mark.skip() + @parametrize + def test_streaming_response_move(self, client: Kernel) -> None: + with client.browsers.fs.with_streaming_response.move( + id="id", + dest_path="/J!", + src_path="/J!", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + f = response.parse() + assert f is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + def test_path_params_move(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.fs.with_raw_response.move( + id="", + dest_path="/J!", + src_path="/J!", + ) + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_method_read_file(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/id/fs/read_file").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + f = client.browsers.fs.read_file( + id="id", + path="/J!", + ) + assert f.is_closed + assert f.json() == {"foo": "bar"} + assert cast(Any, f.is_closed) is True + assert isinstance(f, BinaryAPIResponse) + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_raw_response_read_file(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/id/fs/read_file").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + f = client.browsers.fs.with_raw_response.read_file( + id="id", + path="/J!", + ) + + assert f.is_closed is True + assert f.http_request.headers.get("X-Stainless-Lang") == "python" + assert f.json() == {"foo": "bar"} + assert isinstance(f, BinaryAPIResponse) + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_streaming_response_read_file(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/id/fs/read_file").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + with client.browsers.fs.with_streaming_response.read_file( + id="id", + path="/J!", + ) as f: + assert not f.is_closed + assert f.http_request.headers.get("X-Stainless-Lang") == "python" + + assert f.json() == {"foo": "bar"} + assert cast(Any, f.is_closed) is True + assert isinstance(f, StreamedBinaryAPIResponse) + + assert cast(Any, f.is_closed) is True + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_path_params_read_file(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.fs.with_raw_response.read_file( + id="", + path="/J!", + ) + + @pytest.mark.skip() + @parametrize + def test_method_set_file_permissions(self, client: Kernel) -> None: + f = client.browsers.fs.set_file_permissions( + id="id", + mode="0611", + path="/J!", + ) + assert f is None + + @pytest.mark.skip() + @parametrize + def test_method_set_file_permissions_with_all_params(self, client: Kernel) -> None: + f = client.browsers.fs.set_file_permissions( + id="id", + mode="0611", + path="/J!", + group="group", + owner="owner", + ) + assert f is None + + @pytest.mark.skip() + @parametrize + def test_raw_response_set_file_permissions(self, client: Kernel) -> None: + response = client.browsers.fs.with_raw_response.set_file_permissions( + id="id", + mode="0611", + path="/J!", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + f = response.parse() + assert f is None + + @pytest.mark.skip() + @parametrize + def test_streaming_response_set_file_permissions(self, client: Kernel) -> None: + with client.browsers.fs.with_streaming_response.set_file_permissions( + id="id", + mode="0611", + path="/J!", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + f = response.parse() + assert f is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + def test_path_params_set_file_permissions(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.fs.with_raw_response.set_file_permissions( + id="", + mode="0611", + path="/J!", + ) + + @pytest.mark.skip() + @parametrize + def test_method_write_file(self, client: Kernel) -> None: + f = client.browsers.fs.write_file( + id="id", + contents=b"raw file contents", + path="/J!", + ) + assert f is None + + @pytest.mark.skip() + @parametrize + def test_method_write_file_with_all_params(self, client: Kernel) -> None: + f = client.browsers.fs.write_file( + id="id", + contents=b"raw file contents", + path="/J!", + mode="0611", + ) + assert f is None + + @pytest.mark.skip() + @parametrize + def test_raw_response_write_file(self, client: Kernel) -> None: + response = client.browsers.fs.with_raw_response.write_file( + id="id", + contents=b"raw file contents", + path="/J!", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + f = response.parse() + assert f is None + + @pytest.mark.skip() + @parametrize + def test_streaming_response_write_file(self, client: Kernel) -> None: + with client.browsers.fs.with_streaming_response.write_file( + id="id", + contents=b"raw file contents", + path="/J!", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + f = response.parse() + assert f is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + def test_path_params_write_file(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.fs.with_raw_response.write_file( + id="", + contents=b"raw file contents", + path="/J!", + ) + + +class TestAsyncFs: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip() + @parametrize + async def test_method_create_directory(self, async_client: AsyncKernel) -> None: + f = await async_client.browsers.fs.create_directory( + id="id", + path="/J!", + ) + assert f is None + + @pytest.mark.skip() + @parametrize + async def test_method_create_directory_with_all_params(self, async_client: AsyncKernel) -> None: + f = await async_client.browsers.fs.create_directory( + id="id", + path="/J!", + mode="0611", + ) + assert f is None + + @pytest.mark.skip() + @parametrize + async def test_raw_response_create_directory(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.fs.with_raw_response.create_directory( + id="id", + path="/J!", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + f = await response.parse() + assert f is None + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_create_directory(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.fs.with_streaming_response.create_directory( + id="id", + path="/J!", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + f = await response.parse() + assert f is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + async def test_path_params_create_directory(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.fs.with_raw_response.create_directory( + id="", + path="/J!", + ) + + @pytest.mark.skip() + @parametrize + async def test_method_delete_directory(self, async_client: AsyncKernel) -> None: + f = await async_client.browsers.fs.delete_directory( + id="id", + path="/J!", + ) + assert f is None + + @pytest.mark.skip() + @parametrize + async def test_raw_response_delete_directory(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.fs.with_raw_response.delete_directory( + id="id", + path="/J!", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + f = await response.parse() + assert f is None + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_delete_directory(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.fs.with_streaming_response.delete_directory( + id="id", + path="/J!", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + f = await response.parse() + assert f is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + async def test_path_params_delete_directory(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.fs.with_raw_response.delete_directory( + id="", + path="/J!", + ) + + @pytest.mark.skip() + @parametrize + async def test_method_delete_file(self, async_client: AsyncKernel) -> None: + f = await async_client.browsers.fs.delete_file( + id="id", + path="/J!", + ) + assert f is None + + @pytest.mark.skip() + @parametrize + async def test_raw_response_delete_file(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.fs.with_raw_response.delete_file( + id="id", + path="/J!", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + f = await response.parse() + assert f is None + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_delete_file(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.fs.with_streaming_response.delete_file( + id="id", + path="/J!", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + f = await response.parse() + assert f is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + async def test_path_params_delete_file(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.fs.with_raw_response.delete_file( + id="", + path="/J!", + ) + + @pytest.mark.skip() + @parametrize + async def test_method_file_info(self, async_client: AsyncKernel) -> None: + f = await async_client.browsers.fs.file_info( + id="id", + path="/J!", + ) + assert_matches_type(FFileInfoResponse, f, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_raw_response_file_info(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.fs.with_raw_response.file_info( + id="id", + path="/J!", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + f = await response.parse() + assert_matches_type(FFileInfoResponse, f, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_file_info(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.fs.with_streaming_response.file_info( + id="id", + path="/J!", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + f = await response.parse() + assert_matches_type(FFileInfoResponse, f, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + async def test_path_params_file_info(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.fs.with_raw_response.file_info( + id="", + path="/J!", + ) + + @pytest.mark.skip() + @parametrize + async def test_method_list_files(self, async_client: AsyncKernel) -> None: + f = await async_client.browsers.fs.list_files( + id="id", + path="/J!", + ) + assert_matches_type(FListFilesResponse, f, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_raw_response_list_files(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.fs.with_raw_response.list_files( + id="id", + path="/J!", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + f = await response.parse() + assert_matches_type(FListFilesResponse, f, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_list_files(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.fs.with_streaming_response.list_files( + id="id", + path="/J!", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + f = await response.parse() + assert_matches_type(FListFilesResponse, f, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + async def test_path_params_list_files(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.fs.with_raw_response.list_files( + id="", + path="/J!", + ) + + @pytest.mark.skip() + @parametrize + async def test_method_move(self, async_client: AsyncKernel) -> None: + f = await async_client.browsers.fs.move( + id="id", + dest_path="/J!", + src_path="/J!", + ) + assert f is None + + @pytest.mark.skip() + @parametrize + async def test_raw_response_move(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.fs.with_raw_response.move( + id="id", + dest_path="/J!", + src_path="/J!", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + f = await response.parse() + assert f is None + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_move(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.fs.with_streaming_response.move( + id="id", + dest_path="/J!", + src_path="/J!", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + f = await response.parse() + assert f is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + async def test_path_params_move(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.fs.with_raw_response.move( + id="", + dest_path="/J!", + src_path="/J!", + ) + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_method_read_file(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/id/fs/read_file").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + f = await async_client.browsers.fs.read_file( + id="id", + path="/J!", + ) + assert f.is_closed + assert await f.json() == {"foo": "bar"} + assert cast(Any, f.is_closed) is True + assert isinstance(f, AsyncBinaryAPIResponse) + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_raw_response_read_file(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/id/fs/read_file").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + f = await async_client.browsers.fs.with_raw_response.read_file( + id="id", + path="/J!", + ) + + assert f.is_closed is True + assert f.http_request.headers.get("X-Stainless-Lang") == "python" + assert await f.json() == {"foo": "bar"} + assert isinstance(f, AsyncBinaryAPIResponse) + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_streaming_response_read_file(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/id/fs/read_file").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + async with async_client.browsers.fs.with_streaming_response.read_file( + id="id", + path="/J!", + ) as f: + assert not f.is_closed + assert f.http_request.headers.get("X-Stainless-Lang") == "python" + + assert await f.json() == {"foo": "bar"} + assert cast(Any, f.is_closed) is True + assert isinstance(f, AsyncStreamedBinaryAPIResponse) + + assert cast(Any, f.is_closed) is True + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_path_params_read_file(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.fs.with_raw_response.read_file( + id="", + path="/J!", + ) + + @pytest.mark.skip() + @parametrize + async def test_method_set_file_permissions(self, async_client: AsyncKernel) -> None: + f = await async_client.browsers.fs.set_file_permissions( + id="id", + mode="0611", + path="/J!", + ) + assert f is None + + @pytest.mark.skip() + @parametrize + async def test_method_set_file_permissions_with_all_params(self, async_client: AsyncKernel) -> None: + f = await async_client.browsers.fs.set_file_permissions( + id="id", + mode="0611", + path="/J!", + group="group", + owner="owner", + ) + assert f is None + + @pytest.mark.skip() + @parametrize + async def test_raw_response_set_file_permissions(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.fs.with_raw_response.set_file_permissions( + id="id", + mode="0611", + path="/J!", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + f = await response.parse() + assert f is None + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_set_file_permissions(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.fs.with_streaming_response.set_file_permissions( + id="id", + mode="0611", + path="/J!", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + f = await response.parse() + assert f is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + async def test_path_params_set_file_permissions(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.fs.with_raw_response.set_file_permissions( + id="", + mode="0611", + path="/J!", + ) + + @pytest.mark.skip() + @parametrize + async def test_method_write_file(self, async_client: AsyncKernel) -> None: + f = await async_client.browsers.fs.write_file( + id="id", + contents=b"raw file contents", + path="/J!", + ) + assert f is None + + @pytest.mark.skip() + @parametrize + async def test_method_write_file_with_all_params(self, async_client: AsyncKernel) -> None: + f = await async_client.browsers.fs.write_file( + id="id", + contents=b"raw file contents", + path="/J!", + mode="0611", + ) + assert f is None + + @pytest.mark.skip() + @parametrize + async def test_raw_response_write_file(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.fs.with_raw_response.write_file( + id="id", + contents=b"raw file contents", + path="/J!", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + f = await response.parse() + assert f is None + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_write_file(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.fs.with_streaming_response.write_file( + id="id", + contents=b"raw file contents", + path="/J!", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + f = await response.parse() + assert f is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + async def test_path_params_write_file(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.fs.with_raw_response.write_file( + id="", + contents=b"raw file contents", + path="/J!", + ) From 5c2e6aa49b9b59fbd83c185612f265be51eaa668 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 13:41:08 +0000 Subject: [PATCH 139/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index a3bdfd2f..6d78745c 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.8.3" + ".": "0.9.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index a4c4b29d..49f04c95 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.8.3" +version = "0.9.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 98240d71..a21b0438 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.8.3" # x-release-please-version +__version__ = "0.9.0" # x-release-please-version From f41cb417f905d829064ccd3bf1c658c92a58125f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 9 Aug 2025 03:50:38 +0000 Subject: [PATCH 140/448] chore: update @stainless-api/prism-cli to v5.15.0 --- scripts/mock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/mock b/scripts/mock index d2814ae6..0b28f6ea 100755 --- a/scripts/mock +++ b/scripts/mock @@ -21,7 +21,7 @@ echo "==> Starting mock server with URL ${URL}" # Run prism mock on the given spec if [ "$1" == "--daemon" ]; then - npm exec --package=@stainless-api/prism-cli@5.8.5 -- prism mock "$URL" &> .prism.log & + npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & # Wait for server to come online echo -n "Waiting for server" @@ -37,5 +37,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stainless-api/prism-cli@5.8.5 -- prism mock "$URL" + npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" fi From 053ed2009687c1c436f590356acf278cac9ccba2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 9 Aug 2025 03:54:18 +0000 Subject: [PATCH 141/448] chore(internal): update comment in script --- scripts/test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/test b/scripts/test index 2b878456..dbeda2d2 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! prism_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the prism command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stoplight/prism-cli@~5.3.2 -- prism mock path/to/your.openapi.yml${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}" echo exit 1 From edf97a95ee5376c909315e2ba442408a82146341 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 23:58:46 +0000 Subject: [PATCH 142/448] feat(api): add browser ttls --- .stats.yml | 4 ++-- src/kernel/resources/browsers/browsers.py | 12 ++++++++++++ src/kernel/types/browser_create_params.py | 7 +++++++ src/kernel/types/browser_create_response.py | 9 +++++++++ src/kernel/types/browser_list_response.py | 9 +++++++++ src/kernel/types/browser_retrieve_response.py | 9 +++++++++ tests/api_resources/test_browsers.py | 2 ++ 7 files changed, 50 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 062204b4..86b4afde 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 31 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-e907afeabfeea49dedd783112ac3fd29267bc86f3d594f89ba9a2abf2bcbc9d8.yml -openapi_spec_hash: 060ca6288c1a09b6d1bdf207a0011165 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-6f4aab5f0db80d6ce30ef40274eee347cce0a9465e7f1e5077f8f4a085251ddf.yml +openapi_spec_hash: 8e83254243d1620b80a0dc8aa212ee0d config_hash: f67e4b33b2fb30c1405ee2fff8096320 diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 6d29c9e9..559e0948 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -75,6 +75,7 @@ def create( invocation_id: str | NotGiven = NOT_GIVEN, persistence: BrowserPersistenceParam | NotGiven = NOT_GIVEN, stealth: bool | NotGiven = NOT_GIVEN, + timeout_seconds: 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, @@ -96,6 +97,10 @@ def create( stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. + timeout_seconds: The number of seconds of inactivity before the browser session is terminated. + Only applicable to non-persistent browsers. Activity includes CDP connections + and live view connections. Defaults to 60 seconds. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -112,6 +117,7 @@ def create( "invocation_id": invocation_id, "persistence": persistence, "stealth": stealth, + "timeout_seconds": timeout_seconds, }, browser_create_params.BrowserCreateParams, ), @@ -281,6 +287,7 @@ async def create( invocation_id: str | NotGiven = NOT_GIVEN, persistence: BrowserPersistenceParam | NotGiven = NOT_GIVEN, stealth: bool | NotGiven = NOT_GIVEN, + timeout_seconds: 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, @@ -302,6 +309,10 @@ async def create( stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. + timeout_seconds: The number of seconds of inactivity before the browser session is terminated. + Only applicable to non-persistent browsers. Activity includes CDP connections + and live view connections. Defaults to 60 seconds. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -318,6 +329,7 @@ async def create( "invocation_id": invocation_id, "persistence": persistence, "stealth": stealth, + "timeout_seconds": timeout_seconds, }, browser_create_params.BrowserCreateParams, ), diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index 746a92f9..140dac02 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -27,3 +27,10 @@ class BrowserCreateParams(TypedDict, total=False): If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. """ + + timeout_seconds: int + """The number of seconds of inactivity before the browser session is terminated. + + Only applicable to non-persistent browsers. Activity includes CDP connections + and live view connections. Defaults to 60 seconds. + """ diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index afba2b32..7b7b2ab5 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -12,9 +12,18 @@ class BrowserCreateResponse(BaseModel): cdp_ws_url: str """Websocket URL for Chrome DevTools Protocol connections to the browser session""" + headless: bool + """Indicates whether the browser session is headless.""" + session_id: str """Unique identifier for the browser session""" + stealth: bool + """Indicates whether the browser session is stealth.""" + + timeout_seconds: int + """The number of seconds of inactivity before the browser session is terminated.""" + browser_live_view_url: Optional[str] = None """Remote URL for live viewing the browser session. diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index 43c8d924..22fe230b 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -13,9 +13,18 @@ class BrowserListResponseItem(BaseModel): cdp_ws_url: str """Websocket URL for Chrome DevTools Protocol connections to the browser session""" + headless: bool + """Indicates whether the browser session is headless.""" + session_id: str """Unique identifier for the browser session""" + stealth: bool + """Indicates whether the browser session is stealth.""" + + timeout_seconds: int + """The number of seconds of inactivity before the browser session is terminated.""" + browser_live_view_url: Optional[str] = None """Remote URL for live viewing the browser session. diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 45cf74b1..74084b5f 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -12,9 +12,18 @@ class BrowserRetrieveResponse(BaseModel): cdp_ws_url: str """Websocket URL for Chrome DevTools Protocol connections to the browser session""" + headless: bool + """Indicates whether the browser session is headless.""" + session_id: str """Unique identifier for the browser session""" + stealth: bool + """Indicates whether the browser session is stealth.""" + + timeout_seconds: int + """The number of seconds of inactivity before the browser session is terminated.""" + browser_live_view_url: Optional[str] = None """Remote URL for live viewing the browser session. diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index 8f990bea..38020d46 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -35,6 +35,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: invocation_id="rr33xuugxj9h0bkf1rdt2bet", persistence={"id": "my-awesome-browser-for-user-1234"}, stealth=True, + timeout_seconds=0, ) assert_matches_type(BrowserCreateResponse, browser, path=["response"]) @@ -226,6 +227,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> invocation_id="rr33xuugxj9h0bkf1rdt2bet", persistence={"id": "my-awesome-browser-for-user-1234"}, stealth=True, + timeout_seconds=0, ) assert_matches_type(BrowserCreateResponse, browser, path=["response"]) From 58ea21e2f2ab8c95af006d8d0e364fbe26a295bd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 02:13:09 +0000 Subject: [PATCH 143/448] chore(internal): codegen related update --- tests/api_resources/browsers/fs/test_watch.py | 68 +++----- tests/api_resources/browsers/test_fs.py | 148 +++++++++--------- tests/api_resources/browsers/test_replays.py | 60 +++---- tests/api_resources/test_apps.py | 16 +- tests/api_resources/test_browsers.py | 72 ++++----- tests/api_resources/test_deployments.py | 88 ++++------- tests/api_resources/test_invocations.py | 100 +++++------- 7 files changed, 242 insertions(+), 310 deletions(-) diff --git a/tests/api_resources/browsers/fs/test_watch.py b/tests/api_resources/browsers/fs/test_watch.py index b815c8a5..683e1549 100644 --- a/tests/api_resources/browsers/fs/test_watch.py +++ b/tests/api_resources/browsers/fs/test_watch.py @@ -17,9 +17,7 @@ class TestWatch: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") @parametrize def test_method_events(self, client: Kernel) -> None: watch_stream = client.browsers.fs.watch.events( @@ -28,9 +26,7 @@ def test_method_events(self, client: Kernel) -> None: ) watch_stream.response.close() - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") @parametrize def test_raw_response_events(self, client: Kernel) -> None: response = client.browsers.fs.watch.with_raw_response.events( @@ -42,9 +38,7 @@ def test_raw_response_events(self, client: Kernel) -> None: stream = response.parse() stream.close() - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") @parametrize def test_streaming_response_events(self, client: Kernel) -> None: with client.browsers.fs.watch.with_streaming_response.events( @@ -59,9 +53,7 @@ def test_streaming_response_events(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") @parametrize def test_path_params_events(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -76,7 +68,7 @@ def test_path_params_events(self, client: Kernel) -> None: id="id", ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_start(self, client: Kernel) -> None: watch = client.browsers.fs.watch.start( @@ -85,7 +77,7 @@ def test_method_start(self, client: Kernel) -> None: ) assert_matches_type(WatchStartResponse, watch, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_start_with_all_params(self, client: Kernel) -> None: watch = client.browsers.fs.watch.start( @@ -95,7 +87,7 @@ def test_method_start_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(WatchStartResponse, watch, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_start(self, client: Kernel) -> None: response = client.browsers.fs.watch.with_raw_response.start( @@ -108,7 +100,7 @@ def test_raw_response_start(self, client: Kernel) -> None: watch = response.parse() assert_matches_type(WatchStartResponse, watch, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_start(self, client: Kernel) -> None: with client.browsers.fs.watch.with_streaming_response.start( @@ -123,7 +115,7 @@ def test_streaming_response_start(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_path_params_start(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -132,7 +124,7 @@ def test_path_params_start(self, client: Kernel) -> None: path="path", ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_stop(self, client: Kernel) -> None: watch = client.browsers.fs.watch.stop( @@ -141,7 +133,7 @@ def test_method_stop(self, client: Kernel) -> None: ) assert watch is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_stop(self, client: Kernel) -> None: response = client.browsers.fs.watch.with_raw_response.stop( @@ -154,7 +146,7 @@ def test_raw_response_stop(self, client: Kernel) -> None: watch = response.parse() assert watch is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_stop(self, client: Kernel) -> None: with client.browsers.fs.watch.with_streaming_response.stop( @@ -169,7 +161,7 @@ def test_streaming_response_stop(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_path_params_stop(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -190,9 +182,7 @@ class TestAsyncWatch: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") @parametrize async def test_method_events(self, async_client: AsyncKernel) -> None: watch_stream = await async_client.browsers.fs.watch.events( @@ -201,9 +191,7 @@ async def test_method_events(self, async_client: AsyncKernel) -> None: ) await watch_stream.response.aclose() - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") @parametrize async def test_raw_response_events(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.fs.watch.with_raw_response.events( @@ -215,9 +203,7 @@ async def test_raw_response_events(self, async_client: AsyncKernel) -> None: stream = await response.parse() await stream.close() - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") @parametrize async def test_streaming_response_events(self, async_client: AsyncKernel) -> None: async with async_client.browsers.fs.watch.with_streaming_response.events( @@ -232,9 +218,7 @@ async def test_streaming_response_events(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") @parametrize async def test_path_params_events(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -249,7 +233,7 @@ async def test_path_params_events(self, async_client: AsyncKernel) -> None: id="id", ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_start(self, async_client: AsyncKernel) -> None: watch = await async_client.browsers.fs.watch.start( @@ -258,7 +242,7 @@ async def test_method_start(self, async_client: AsyncKernel) -> None: ) assert_matches_type(WatchStartResponse, watch, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_start_with_all_params(self, async_client: AsyncKernel) -> None: watch = await async_client.browsers.fs.watch.start( @@ -268,7 +252,7 @@ async def test_method_start_with_all_params(self, async_client: AsyncKernel) -> ) assert_matches_type(WatchStartResponse, watch, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_start(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.fs.watch.with_raw_response.start( @@ -281,7 +265,7 @@ async def test_raw_response_start(self, async_client: AsyncKernel) -> None: watch = await response.parse() assert_matches_type(WatchStartResponse, watch, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_start(self, async_client: AsyncKernel) -> None: async with async_client.browsers.fs.watch.with_streaming_response.start( @@ -296,7 +280,7 @@ async def test_streaming_response_start(self, async_client: AsyncKernel) -> None assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_path_params_start(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -305,7 +289,7 @@ async def test_path_params_start(self, async_client: AsyncKernel) -> None: path="path", ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_stop(self, async_client: AsyncKernel) -> None: watch = await async_client.browsers.fs.watch.stop( @@ -314,7 +298,7 @@ async def test_method_stop(self, async_client: AsyncKernel) -> None: ) assert watch is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_stop(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.fs.watch.with_raw_response.stop( @@ -327,7 +311,7 @@ async def test_raw_response_stop(self, async_client: AsyncKernel) -> None: watch = await response.parse() assert watch is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_stop(self, async_client: AsyncKernel) -> None: async with async_client.browsers.fs.watch.with_streaming_response.stop( @@ -342,7 +326,7 @@ async def test_streaming_response_stop(self, async_client: AsyncKernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_path_params_stop(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): diff --git a/tests/api_resources/browsers/test_fs.py b/tests/api_resources/browsers/test_fs.py index c82e09d2..24860c90 100644 --- a/tests/api_resources/browsers/test_fs.py +++ b/tests/api_resources/browsers/test_fs.py @@ -28,7 +28,7 @@ class TestFs: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_create_directory(self, client: Kernel) -> None: f = client.browsers.fs.create_directory( @@ -37,7 +37,7 @@ def test_method_create_directory(self, client: Kernel) -> None: ) assert f is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_create_directory_with_all_params(self, client: Kernel) -> None: f = client.browsers.fs.create_directory( @@ -47,7 +47,7 @@ def test_method_create_directory_with_all_params(self, client: Kernel) -> None: ) assert f is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_create_directory(self, client: Kernel) -> None: response = client.browsers.fs.with_raw_response.create_directory( @@ -60,7 +60,7 @@ def test_raw_response_create_directory(self, client: Kernel) -> None: f = response.parse() assert f is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_create_directory(self, client: Kernel) -> None: with client.browsers.fs.with_streaming_response.create_directory( @@ -75,7 +75,7 @@ def test_streaming_response_create_directory(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_path_params_create_directory(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -84,7 +84,7 @@ def test_path_params_create_directory(self, client: Kernel) -> None: path="/J!", ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_delete_directory(self, client: Kernel) -> None: f = client.browsers.fs.delete_directory( @@ -93,7 +93,7 @@ def test_method_delete_directory(self, client: Kernel) -> None: ) assert f is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_delete_directory(self, client: Kernel) -> None: response = client.browsers.fs.with_raw_response.delete_directory( @@ -106,7 +106,7 @@ def test_raw_response_delete_directory(self, client: Kernel) -> None: f = response.parse() assert f is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_delete_directory(self, client: Kernel) -> None: with client.browsers.fs.with_streaming_response.delete_directory( @@ -121,7 +121,7 @@ def test_streaming_response_delete_directory(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_path_params_delete_directory(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -130,7 +130,7 @@ def test_path_params_delete_directory(self, client: Kernel) -> None: path="/J!", ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_delete_file(self, client: Kernel) -> None: f = client.browsers.fs.delete_file( @@ -139,7 +139,7 @@ def test_method_delete_file(self, client: Kernel) -> None: ) assert f is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_delete_file(self, client: Kernel) -> None: response = client.browsers.fs.with_raw_response.delete_file( @@ -152,7 +152,7 @@ def test_raw_response_delete_file(self, client: Kernel) -> None: f = response.parse() assert f is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_delete_file(self, client: Kernel) -> None: with client.browsers.fs.with_streaming_response.delete_file( @@ -167,7 +167,7 @@ def test_streaming_response_delete_file(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_path_params_delete_file(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -176,7 +176,7 @@ def test_path_params_delete_file(self, client: Kernel) -> None: path="/J!", ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_file_info(self, client: Kernel) -> None: f = client.browsers.fs.file_info( @@ -185,7 +185,7 @@ def test_method_file_info(self, client: Kernel) -> None: ) assert_matches_type(FFileInfoResponse, f, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_file_info(self, client: Kernel) -> None: response = client.browsers.fs.with_raw_response.file_info( @@ -198,7 +198,7 @@ def test_raw_response_file_info(self, client: Kernel) -> None: f = response.parse() assert_matches_type(FFileInfoResponse, f, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_file_info(self, client: Kernel) -> None: with client.browsers.fs.with_streaming_response.file_info( @@ -213,7 +213,7 @@ def test_streaming_response_file_info(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_path_params_file_info(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -222,7 +222,7 @@ def test_path_params_file_info(self, client: Kernel) -> None: path="/J!", ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_list_files(self, client: Kernel) -> None: f = client.browsers.fs.list_files( @@ -231,7 +231,7 @@ def test_method_list_files(self, client: Kernel) -> None: ) assert_matches_type(FListFilesResponse, f, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_list_files(self, client: Kernel) -> None: response = client.browsers.fs.with_raw_response.list_files( @@ -244,7 +244,7 @@ def test_raw_response_list_files(self, client: Kernel) -> None: f = response.parse() assert_matches_type(FListFilesResponse, f, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_list_files(self, client: Kernel) -> None: with client.browsers.fs.with_streaming_response.list_files( @@ -259,7 +259,7 @@ def test_streaming_response_list_files(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_path_params_list_files(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -268,7 +268,7 @@ def test_path_params_list_files(self, client: Kernel) -> None: path="/J!", ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_move(self, client: Kernel) -> None: f = client.browsers.fs.move( @@ -278,7 +278,7 @@ def test_method_move(self, client: Kernel) -> None: ) assert f is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_move(self, client: Kernel) -> None: response = client.browsers.fs.with_raw_response.move( @@ -292,7 +292,7 @@ def test_raw_response_move(self, client: Kernel) -> None: f = response.parse() assert f is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_move(self, client: Kernel) -> None: with client.browsers.fs.with_streaming_response.move( @@ -308,7 +308,7 @@ def test_streaming_response_move(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_path_params_move(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -318,7 +318,6 @@ def test_path_params_move(self, client: Kernel) -> None: src_path="/J!", ) - @pytest.mark.skip() @parametrize @pytest.mark.respx(base_url=base_url) def test_method_read_file(self, client: Kernel, respx_mock: MockRouter) -> None: @@ -332,7 +331,6 @@ def test_method_read_file(self, client: Kernel, respx_mock: MockRouter) -> None: assert cast(Any, f.is_closed) is True assert isinstance(f, BinaryAPIResponse) - @pytest.mark.skip() @parametrize @pytest.mark.respx(base_url=base_url) def test_raw_response_read_file(self, client: Kernel, respx_mock: MockRouter) -> None: @@ -348,7 +346,6 @@ def test_raw_response_read_file(self, client: Kernel, respx_mock: MockRouter) -> assert f.json() == {"foo": "bar"} assert isinstance(f, BinaryAPIResponse) - @pytest.mark.skip() @parametrize @pytest.mark.respx(base_url=base_url) def test_streaming_response_read_file(self, client: Kernel, respx_mock: MockRouter) -> None: @@ -366,7 +363,6 @@ def test_streaming_response_read_file(self, client: Kernel, respx_mock: MockRout assert cast(Any, f.is_closed) is True - @pytest.mark.skip() @parametrize @pytest.mark.respx(base_url=base_url) def test_path_params_read_file(self, client: Kernel) -> None: @@ -376,7 +372,7 @@ def test_path_params_read_file(self, client: Kernel) -> None: path="/J!", ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_set_file_permissions(self, client: Kernel) -> None: f = client.browsers.fs.set_file_permissions( @@ -386,7 +382,7 @@ def test_method_set_file_permissions(self, client: Kernel) -> None: ) assert f is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_set_file_permissions_with_all_params(self, client: Kernel) -> None: f = client.browsers.fs.set_file_permissions( @@ -398,7 +394,7 @@ def test_method_set_file_permissions_with_all_params(self, client: Kernel) -> No ) assert f is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_set_file_permissions(self, client: Kernel) -> None: response = client.browsers.fs.with_raw_response.set_file_permissions( @@ -412,7 +408,7 @@ def test_raw_response_set_file_permissions(self, client: Kernel) -> None: f = response.parse() assert f is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_set_file_permissions(self, client: Kernel) -> None: with client.browsers.fs.with_streaming_response.set_file_permissions( @@ -428,7 +424,7 @@ def test_streaming_response_set_file_permissions(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_path_params_set_file_permissions(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -438,7 +434,7 @@ def test_path_params_set_file_permissions(self, client: Kernel) -> None: path="/J!", ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_write_file(self, client: Kernel) -> None: f = client.browsers.fs.write_file( @@ -448,7 +444,7 @@ def test_method_write_file(self, client: Kernel) -> None: ) assert f is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_write_file_with_all_params(self, client: Kernel) -> None: f = client.browsers.fs.write_file( @@ -459,7 +455,7 @@ def test_method_write_file_with_all_params(self, client: Kernel) -> None: ) assert f is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_write_file(self, client: Kernel) -> None: response = client.browsers.fs.with_raw_response.write_file( @@ -473,7 +469,7 @@ def test_raw_response_write_file(self, client: Kernel) -> None: f = response.parse() assert f is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_write_file(self, client: Kernel) -> None: with client.browsers.fs.with_streaming_response.write_file( @@ -489,7 +485,7 @@ def test_streaming_response_write_file(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_path_params_write_file(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -505,7 +501,7 @@ class TestAsyncFs: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_create_directory(self, async_client: AsyncKernel) -> None: f = await async_client.browsers.fs.create_directory( @@ -514,7 +510,7 @@ async def test_method_create_directory(self, async_client: AsyncKernel) -> None: ) assert f is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_create_directory_with_all_params(self, async_client: AsyncKernel) -> None: f = await async_client.browsers.fs.create_directory( @@ -524,7 +520,7 @@ async def test_method_create_directory_with_all_params(self, async_client: Async ) assert f is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_create_directory(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.fs.with_raw_response.create_directory( @@ -537,7 +533,7 @@ async def test_raw_response_create_directory(self, async_client: AsyncKernel) -> f = await response.parse() assert f is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_create_directory(self, async_client: AsyncKernel) -> None: async with async_client.browsers.fs.with_streaming_response.create_directory( @@ -552,7 +548,7 @@ async def test_streaming_response_create_directory(self, async_client: AsyncKern assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_path_params_create_directory(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -561,7 +557,7 @@ async def test_path_params_create_directory(self, async_client: AsyncKernel) -> path="/J!", ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_delete_directory(self, async_client: AsyncKernel) -> None: f = await async_client.browsers.fs.delete_directory( @@ -570,7 +566,7 @@ async def test_method_delete_directory(self, async_client: AsyncKernel) -> None: ) assert f is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_delete_directory(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.fs.with_raw_response.delete_directory( @@ -583,7 +579,7 @@ async def test_raw_response_delete_directory(self, async_client: AsyncKernel) -> f = await response.parse() assert f is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_delete_directory(self, async_client: AsyncKernel) -> None: async with async_client.browsers.fs.with_streaming_response.delete_directory( @@ -598,7 +594,7 @@ async def test_streaming_response_delete_directory(self, async_client: AsyncKern assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_path_params_delete_directory(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -607,7 +603,7 @@ async def test_path_params_delete_directory(self, async_client: AsyncKernel) -> path="/J!", ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_delete_file(self, async_client: AsyncKernel) -> None: f = await async_client.browsers.fs.delete_file( @@ -616,7 +612,7 @@ async def test_method_delete_file(self, async_client: AsyncKernel) -> None: ) assert f is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_delete_file(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.fs.with_raw_response.delete_file( @@ -629,7 +625,7 @@ async def test_raw_response_delete_file(self, async_client: AsyncKernel) -> None f = await response.parse() assert f is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_delete_file(self, async_client: AsyncKernel) -> None: async with async_client.browsers.fs.with_streaming_response.delete_file( @@ -644,7 +640,7 @@ async def test_streaming_response_delete_file(self, async_client: AsyncKernel) - assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_path_params_delete_file(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -653,7 +649,7 @@ async def test_path_params_delete_file(self, async_client: AsyncKernel) -> None: path="/J!", ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_file_info(self, async_client: AsyncKernel) -> None: f = await async_client.browsers.fs.file_info( @@ -662,7 +658,7 @@ async def test_method_file_info(self, async_client: AsyncKernel) -> None: ) assert_matches_type(FFileInfoResponse, f, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_file_info(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.fs.with_raw_response.file_info( @@ -675,7 +671,7 @@ async def test_raw_response_file_info(self, async_client: AsyncKernel) -> None: f = await response.parse() assert_matches_type(FFileInfoResponse, f, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_file_info(self, async_client: AsyncKernel) -> None: async with async_client.browsers.fs.with_streaming_response.file_info( @@ -690,7 +686,7 @@ async def test_streaming_response_file_info(self, async_client: AsyncKernel) -> assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_path_params_file_info(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -699,7 +695,7 @@ async def test_path_params_file_info(self, async_client: AsyncKernel) -> None: path="/J!", ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_list_files(self, async_client: AsyncKernel) -> None: f = await async_client.browsers.fs.list_files( @@ -708,7 +704,7 @@ async def test_method_list_files(self, async_client: AsyncKernel) -> None: ) assert_matches_type(FListFilesResponse, f, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_list_files(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.fs.with_raw_response.list_files( @@ -721,7 +717,7 @@ async def test_raw_response_list_files(self, async_client: AsyncKernel) -> None: f = await response.parse() assert_matches_type(FListFilesResponse, f, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_list_files(self, async_client: AsyncKernel) -> None: async with async_client.browsers.fs.with_streaming_response.list_files( @@ -736,7 +732,7 @@ async def test_streaming_response_list_files(self, async_client: AsyncKernel) -> assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_path_params_list_files(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -745,7 +741,7 @@ async def test_path_params_list_files(self, async_client: AsyncKernel) -> None: path="/J!", ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_move(self, async_client: AsyncKernel) -> None: f = await async_client.browsers.fs.move( @@ -755,7 +751,7 @@ async def test_method_move(self, async_client: AsyncKernel) -> None: ) assert f is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_move(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.fs.with_raw_response.move( @@ -769,7 +765,7 @@ async def test_raw_response_move(self, async_client: AsyncKernel) -> None: f = await response.parse() assert f is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_move(self, async_client: AsyncKernel) -> None: async with async_client.browsers.fs.with_streaming_response.move( @@ -785,7 +781,7 @@ async def test_streaming_response_move(self, async_client: AsyncKernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_path_params_move(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -795,7 +791,6 @@ async def test_path_params_move(self, async_client: AsyncKernel) -> None: src_path="/J!", ) - @pytest.mark.skip() @parametrize @pytest.mark.respx(base_url=base_url) async def test_method_read_file(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: @@ -809,7 +804,6 @@ async def test_method_read_file(self, async_client: AsyncKernel, respx_mock: Moc assert cast(Any, f.is_closed) is True assert isinstance(f, AsyncBinaryAPIResponse) - @pytest.mark.skip() @parametrize @pytest.mark.respx(base_url=base_url) async def test_raw_response_read_file(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: @@ -825,7 +819,6 @@ async def test_raw_response_read_file(self, async_client: AsyncKernel, respx_moc assert await f.json() == {"foo": "bar"} assert isinstance(f, AsyncBinaryAPIResponse) - @pytest.mark.skip() @parametrize @pytest.mark.respx(base_url=base_url) async def test_streaming_response_read_file(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: @@ -843,7 +836,6 @@ async def test_streaming_response_read_file(self, async_client: AsyncKernel, res assert cast(Any, f.is_closed) is True - @pytest.mark.skip() @parametrize @pytest.mark.respx(base_url=base_url) async def test_path_params_read_file(self, async_client: AsyncKernel) -> None: @@ -853,7 +845,7 @@ async def test_path_params_read_file(self, async_client: AsyncKernel) -> None: path="/J!", ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_set_file_permissions(self, async_client: AsyncKernel) -> None: f = await async_client.browsers.fs.set_file_permissions( @@ -863,7 +855,7 @@ async def test_method_set_file_permissions(self, async_client: AsyncKernel) -> N ) assert f is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_set_file_permissions_with_all_params(self, async_client: AsyncKernel) -> None: f = await async_client.browsers.fs.set_file_permissions( @@ -875,7 +867,7 @@ async def test_method_set_file_permissions_with_all_params(self, async_client: A ) assert f is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_set_file_permissions(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.fs.with_raw_response.set_file_permissions( @@ -889,7 +881,7 @@ async def test_raw_response_set_file_permissions(self, async_client: AsyncKernel f = await response.parse() assert f is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_set_file_permissions(self, async_client: AsyncKernel) -> None: async with async_client.browsers.fs.with_streaming_response.set_file_permissions( @@ -905,7 +897,7 @@ async def test_streaming_response_set_file_permissions(self, async_client: Async assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_path_params_set_file_permissions(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -915,7 +907,7 @@ async def test_path_params_set_file_permissions(self, async_client: AsyncKernel) path="/J!", ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_write_file(self, async_client: AsyncKernel) -> None: f = await async_client.browsers.fs.write_file( @@ -925,7 +917,7 @@ async def test_method_write_file(self, async_client: AsyncKernel) -> None: ) assert f is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_write_file_with_all_params(self, async_client: AsyncKernel) -> None: f = await async_client.browsers.fs.write_file( @@ -936,7 +928,7 @@ async def test_method_write_file_with_all_params(self, async_client: AsyncKernel ) assert f is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_write_file(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.fs.with_raw_response.write_file( @@ -950,7 +942,7 @@ async def test_raw_response_write_file(self, async_client: AsyncKernel) -> None: f = await response.parse() assert f is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_write_file(self, async_client: AsyncKernel) -> None: async with async_client.browsers.fs.with_streaming_response.write_file( @@ -966,7 +958,7 @@ async def test_streaming_response_write_file(self, async_client: AsyncKernel) -> assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_path_params_write_file(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): diff --git a/tests/api_resources/browsers/test_replays.py b/tests/api_resources/browsers/test_replays.py index 930d008d..df1fed56 100644 --- a/tests/api_resources/browsers/test_replays.py +++ b/tests/api_resources/browsers/test_replays.py @@ -25,7 +25,7 @@ class TestReplays: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_list(self, client: Kernel) -> None: replay = client.browsers.replays.list( @@ -33,7 +33,7 @@ def test_method_list(self, client: Kernel) -> None: ) assert_matches_type(ReplayListResponse, replay, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_list(self, client: Kernel) -> None: response = client.browsers.replays.with_raw_response.list( @@ -45,7 +45,7 @@ def test_raw_response_list(self, client: Kernel) -> None: replay = response.parse() assert_matches_type(ReplayListResponse, replay, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_list(self, client: Kernel) -> None: with client.browsers.replays.with_streaming_response.list( @@ -59,7 +59,7 @@ def test_streaming_response_list(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_path_params_list(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -67,7 +67,6 @@ def test_path_params_list(self, client: Kernel) -> None: "", ) - @pytest.mark.skip() @parametrize @pytest.mark.respx(base_url=base_url) def test_method_download(self, client: Kernel, respx_mock: MockRouter) -> None: @@ -81,7 +80,6 @@ def test_method_download(self, client: Kernel, respx_mock: MockRouter) -> None: assert cast(Any, replay.is_closed) is True assert isinstance(replay, BinaryAPIResponse) - @pytest.mark.skip() @parametrize @pytest.mark.respx(base_url=base_url) def test_raw_response_download(self, client: Kernel, respx_mock: MockRouter) -> None: @@ -97,7 +95,6 @@ def test_raw_response_download(self, client: Kernel, respx_mock: MockRouter) -> assert replay.json() == {"foo": "bar"} assert isinstance(replay, BinaryAPIResponse) - @pytest.mark.skip() @parametrize @pytest.mark.respx(base_url=base_url) def test_streaming_response_download(self, client: Kernel, respx_mock: MockRouter) -> None: @@ -115,7 +112,6 @@ def test_streaming_response_download(self, client: Kernel, respx_mock: MockRoute assert cast(Any, replay.is_closed) is True - @pytest.mark.skip() @parametrize @pytest.mark.respx(base_url=base_url) def test_path_params_download(self, client: Kernel) -> None: @@ -131,7 +127,7 @@ def test_path_params_download(self, client: Kernel) -> None: id="id", ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_start(self, client: Kernel) -> None: replay = client.browsers.replays.start( @@ -139,7 +135,7 @@ def test_method_start(self, client: Kernel) -> None: ) assert_matches_type(ReplayStartResponse, replay, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_start_with_all_params(self, client: Kernel) -> None: replay = client.browsers.replays.start( @@ -149,7 +145,7 @@ def test_method_start_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(ReplayStartResponse, replay, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_start(self, client: Kernel) -> None: response = client.browsers.replays.with_raw_response.start( @@ -161,7 +157,7 @@ def test_raw_response_start(self, client: Kernel) -> None: replay = response.parse() assert_matches_type(ReplayStartResponse, replay, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_start(self, client: Kernel) -> None: with client.browsers.replays.with_streaming_response.start( @@ -175,7 +171,7 @@ def test_streaming_response_start(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_path_params_start(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -183,7 +179,7 @@ def test_path_params_start(self, client: Kernel) -> None: id="", ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_stop(self, client: Kernel) -> None: replay = client.browsers.replays.stop( @@ -192,7 +188,7 @@ def test_method_stop(self, client: Kernel) -> None: ) assert replay is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_stop(self, client: Kernel) -> None: response = client.browsers.replays.with_raw_response.stop( @@ -205,7 +201,7 @@ def test_raw_response_stop(self, client: Kernel) -> None: replay = response.parse() assert replay is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_stop(self, client: Kernel) -> None: with client.browsers.replays.with_streaming_response.stop( @@ -220,7 +216,7 @@ def test_streaming_response_stop(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_path_params_stop(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -241,7 +237,7 @@ class TestAsyncReplays: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_list(self, async_client: AsyncKernel) -> None: replay = await async_client.browsers.replays.list( @@ -249,7 +245,7 @@ async def test_method_list(self, async_client: AsyncKernel) -> None: ) assert_matches_type(ReplayListResponse, replay, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_list(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.replays.with_raw_response.list( @@ -261,7 +257,7 @@ async def test_raw_response_list(self, async_client: AsyncKernel) -> None: replay = await response.parse() assert_matches_type(ReplayListResponse, replay, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: async with async_client.browsers.replays.with_streaming_response.list( @@ -275,7 +271,7 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_path_params_list(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -283,7 +279,6 @@ async def test_path_params_list(self, async_client: AsyncKernel) -> None: "", ) - @pytest.mark.skip() @parametrize @pytest.mark.respx(base_url=base_url) async def test_method_download(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: @@ -297,7 +292,6 @@ async def test_method_download(self, async_client: AsyncKernel, respx_mock: Mock assert cast(Any, replay.is_closed) is True assert isinstance(replay, AsyncBinaryAPIResponse) - @pytest.mark.skip() @parametrize @pytest.mark.respx(base_url=base_url) async def test_raw_response_download(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: @@ -313,7 +307,6 @@ async def test_raw_response_download(self, async_client: AsyncKernel, respx_mock assert await replay.json() == {"foo": "bar"} assert isinstance(replay, AsyncBinaryAPIResponse) - @pytest.mark.skip() @parametrize @pytest.mark.respx(base_url=base_url) async def test_streaming_response_download(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: @@ -331,7 +324,6 @@ async def test_streaming_response_download(self, async_client: AsyncKernel, resp assert cast(Any, replay.is_closed) is True - @pytest.mark.skip() @parametrize @pytest.mark.respx(base_url=base_url) async def test_path_params_download(self, async_client: AsyncKernel) -> None: @@ -347,7 +339,7 @@ async def test_path_params_download(self, async_client: AsyncKernel) -> None: id="id", ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_start(self, async_client: AsyncKernel) -> None: replay = await async_client.browsers.replays.start( @@ -355,7 +347,7 @@ async def test_method_start(self, async_client: AsyncKernel) -> None: ) assert_matches_type(ReplayStartResponse, replay, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_start_with_all_params(self, async_client: AsyncKernel) -> None: replay = await async_client.browsers.replays.start( @@ -365,7 +357,7 @@ async def test_method_start_with_all_params(self, async_client: AsyncKernel) -> ) assert_matches_type(ReplayStartResponse, replay, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_start(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.replays.with_raw_response.start( @@ -377,7 +369,7 @@ async def test_raw_response_start(self, async_client: AsyncKernel) -> None: replay = await response.parse() assert_matches_type(ReplayStartResponse, replay, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_start(self, async_client: AsyncKernel) -> None: async with async_client.browsers.replays.with_streaming_response.start( @@ -391,7 +383,7 @@ async def test_streaming_response_start(self, async_client: AsyncKernel) -> None assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_path_params_start(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -399,7 +391,7 @@ async def test_path_params_start(self, async_client: AsyncKernel) -> None: id="", ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_stop(self, async_client: AsyncKernel) -> None: replay = await async_client.browsers.replays.stop( @@ -408,7 +400,7 @@ async def test_method_stop(self, async_client: AsyncKernel) -> None: ) assert replay is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_stop(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.replays.with_raw_response.stop( @@ -421,7 +413,7 @@ async def test_raw_response_stop(self, async_client: AsyncKernel) -> None: replay = await response.parse() assert replay is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_stop(self, async_client: AsyncKernel) -> None: async with async_client.browsers.replays.with_streaming_response.stop( @@ -436,7 +428,7 @@ async def test_streaming_response_stop(self, async_client: AsyncKernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_path_params_stop(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): diff --git a/tests/api_resources/test_apps.py b/tests/api_resources/test_apps.py index 05066cdd..5e6db3ba 100644 --- a/tests/api_resources/test_apps.py +++ b/tests/api_resources/test_apps.py @@ -17,13 +17,13 @@ class TestApps: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_list(self, client: Kernel) -> None: app = client.apps.list() assert_matches_type(AppListResponse, app, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_list_with_all_params(self, client: Kernel) -> None: app = client.apps.list( @@ -32,7 +32,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(AppListResponse, app, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_list(self, client: Kernel) -> None: response = client.apps.with_raw_response.list() @@ -42,7 +42,7 @@ def test_raw_response_list(self, client: Kernel) -> None: app = response.parse() assert_matches_type(AppListResponse, app, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_list(self, client: Kernel) -> None: with client.apps.with_streaming_response.list() as response: @@ -60,13 +60,13 @@ class TestAsyncApps: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_list(self, async_client: AsyncKernel) -> None: app = await async_client.apps.list() assert_matches_type(AppListResponse, app, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: app = await async_client.apps.list( @@ -75,7 +75,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N ) assert_matches_type(AppListResponse, app, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_list(self, async_client: AsyncKernel) -> None: response = await async_client.apps.with_raw_response.list() @@ -85,7 +85,7 @@ async def test_raw_response_list(self, async_client: AsyncKernel) -> None: app = await response.parse() assert_matches_type(AppListResponse, app, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: async with async_client.apps.with_streaming_response.list() as response: diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index 38020d46..6f9437f5 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -21,13 +21,13 @@ class TestBrowsers: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_create(self, client: Kernel) -> None: browser = client.browsers.create() assert_matches_type(BrowserCreateResponse, browser, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_create_with_all_params(self, client: Kernel) -> None: browser = client.browsers.create( @@ -39,7 +39,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(BrowserCreateResponse, browser, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_create(self, client: Kernel) -> None: response = client.browsers.with_raw_response.create() @@ -49,7 +49,7 @@ def test_raw_response_create(self, client: Kernel) -> None: browser = response.parse() assert_matches_type(BrowserCreateResponse, browser, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_create(self, client: Kernel) -> None: with client.browsers.with_streaming_response.create() as response: @@ -61,7 +61,7 @@ def test_streaming_response_create(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_retrieve(self, client: Kernel) -> None: browser = client.browsers.retrieve( @@ -69,7 +69,7 @@ def test_method_retrieve(self, client: Kernel) -> None: ) assert_matches_type(BrowserRetrieveResponse, browser, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_retrieve(self, client: Kernel) -> None: response = client.browsers.with_raw_response.retrieve( @@ -81,7 +81,7 @@ def test_raw_response_retrieve(self, client: Kernel) -> None: browser = response.parse() assert_matches_type(BrowserRetrieveResponse, browser, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_retrieve(self, client: Kernel) -> None: with client.browsers.with_streaming_response.retrieve( @@ -95,7 +95,7 @@ def test_streaming_response_retrieve(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_path_params_retrieve(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -103,13 +103,13 @@ def test_path_params_retrieve(self, client: Kernel) -> None: "", ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_list(self, client: Kernel) -> None: browser = client.browsers.list() assert_matches_type(BrowserListResponse, browser, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_list(self, client: Kernel) -> None: response = client.browsers.with_raw_response.list() @@ -119,7 +119,7 @@ def test_raw_response_list(self, client: Kernel) -> None: browser = response.parse() assert_matches_type(BrowserListResponse, browser, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_list(self, client: Kernel) -> None: with client.browsers.with_streaming_response.list() as response: @@ -131,7 +131,7 @@ def test_streaming_response_list(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_delete(self, client: Kernel) -> None: browser = client.browsers.delete( @@ -139,7 +139,7 @@ def test_method_delete(self, client: Kernel) -> None: ) assert browser is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_delete(self, client: Kernel) -> None: response = client.browsers.with_raw_response.delete( @@ -151,7 +151,7 @@ def test_raw_response_delete(self, client: Kernel) -> None: browser = response.parse() assert browser is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_delete(self, client: Kernel) -> None: with client.browsers.with_streaming_response.delete( @@ -165,7 +165,7 @@ def test_streaming_response_delete(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_delete_by_id(self, client: Kernel) -> None: browser = client.browsers.delete_by_id( @@ -173,7 +173,7 @@ def test_method_delete_by_id(self, client: Kernel) -> None: ) assert browser is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_delete_by_id(self, client: Kernel) -> None: response = client.browsers.with_raw_response.delete_by_id( @@ -185,7 +185,7 @@ def test_raw_response_delete_by_id(self, client: Kernel) -> None: browser = response.parse() assert browser is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_delete_by_id(self, client: Kernel) -> None: with client.browsers.with_streaming_response.delete_by_id( @@ -199,7 +199,7 @@ def test_streaming_response_delete_by_id(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_path_params_delete_by_id(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -213,13 +213,13 @@ class TestAsyncBrowsers: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_create(self, async_client: AsyncKernel) -> None: browser = await async_client.browsers.create() assert_matches_type(BrowserCreateResponse, browser, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: browser = await async_client.browsers.create( @@ -231,7 +231,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> ) assert_matches_type(BrowserCreateResponse, browser, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_create(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.with_raw_response.create() @@ -241,7 +241,7 @@ async def test_raw_response_create(self, async_client: AsyncKernel) -> None: browser = await response.parse() assert_matches_type(BrowserCreateResponse, browser, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: async with async_client.browsers.with_streaming_response.create() as response: @@ -253,7 +253,7 @@ async def test_streaming_response_create(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_retrieve(self, async_client: AsyncKernel) -> None: browser = await async_client.browsers.retrieve( @@ -261,7 +261,7 @@ async def test_method_retrieve(self, async_client: AsyncKernel) -> None: ) assert_matches_type(BrowserRetrieveResponse, browser, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.with_raw_response.retrieve( @@ -273,7 +273,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: browser = await response.parse() assert_matches_type(BrowserRetrieveResponse, browser, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: async with async_client.browsers.with_streaming_response.retrieve( @@ -287,7 +287,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> N assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -295,13 +295,13 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: "", ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_list(self, async_client: AsyncKernel) -> None: browser = await async_client.browsers.list() assert_matches_type(BrowserListResponse, browser, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_list(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.with_raw_response.list() @@ -311,7 +311,7 @@ async def test_raw_response_list(self, async_client: AsyncKernel) -> None: browser = await response.parse() assert_matches_type(BrowserListResponse, browser, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: async with async_client.browsers.with_streaming_response.list() as response: @@ -323,7 +323,7 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_delete(self, async_client: AsyncKernel) -> None: browser = await async_client.browsers.delete( @@ -331,7 +331,7 @@ async def test_method_delete(self, async_client: AsyncKernel) -> None: ) assert browser is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.with_raw_response.delete( @@ -343,7 +343,7 @@ async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: browser = await response.parse() assert browser is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None: async with async_client.browsers.with_streaming_response.delete( @@ -357,7 +357,7 @@ async def test_streaming_response_delete(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_delete_by_id(self, async_client: AsyncKernel) -> None: browser = await async_client.browsers.delete_by_id( @@ -365,7 +365,7 @@ async def test_method_delete_by_id(self, async_client: AsyncKernel) -> None: ) assert browser is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_delete_by_id(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.with_raw_response.delete_by_id( @@ -377,7 +377,7 @@ async def test_raw_response_delete_by_id(self, async_client: AsyncKernel) -> Non browser = await response.parse() assert browser is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_delete_by_id(self, async_client: AsyncKernel) -> None: async with async_client.browsers.with_streaming_response.delete_by_id( @@ -391,7 +391,7 @@ async def test_streaming_response_delete_by_id(self, async_client: AsyncKernel) assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_path_params_delete_by_id(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): diff --git a/tests/api_resources/test_deployments.py b/tests/api_resources/test_deployments.py index 32214168..c177978b 100644 --- a/tests/api_resources/test_deployments.py +++ b/tests/api_resources/test_deployments.py @@ -21,7 +21,7 @@ class TestDeployments: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_create(self, client: Kernel) -> None: deployment = client.deployments.create( @@ -30,7 +30,7 @@ def test_method_create(self, client: Kernel) -> None: ) assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_create_with_all_params(self, client: Kernel) -> None: deployment = client.deployments.create( @@ -43,7 +43,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_create(self, client: Kernel) -> None: response = client.deployments.with_raw_response.create( @@ -56,7 +56,7 @@ def test_raw_response_create(self, client: Kernel) -> None: deployment = response.parse() assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_create(self, client: Kernel) -> None: with client.deployments.with_streaming_response.create( @@ -71,7 +71,7 @@ def test_streaming_response_create(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_retrieve(self, client: Kernel) -> None: deployment = client.deployments.retrieve( @@ -79,7 +79,7 @@ def test_method_retrieve(self, client: Kernel) -> None: ) assert_matches_type(DeploymentRetrieveResponse, deployment, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_retrieve(self, client: Kernel) -> None: response = client.deployments.with_raw_response.retrieve( @@ -91,7 +91,7 @@ def test_raw_response_retrieve(self, client: Kernel) -> None: deployment = response.parse() assert_matches_type(DeploymentRetrieveResponse, deployment, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_retrieve(self, client: Kernel) -> None: with client.deployments.with_streaming_response.retrieve( @@ -105,7 +105,7 @@ def test_streaming_response_retrieve(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_path_params_retrieve(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -113,13 +113,13 @@ def test_path_params_retrieve(self, client: Kernel) -> None: "", ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_list(self, client: Kernel) -> None: deployment = client.deployments.list() assert_matches_type(DeploymentListResponse, deployment, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_list_with_all_params(self, client: Kernel) -> None: deployment = client.deployments.list( @@ -127,7 +127,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(DeploymentListResponse, deployment, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_list(self, client: Kernel) -> None: response = client.deployments.with_raw_response.list() @@ -137,7 +137,7 @@ def test_raw_response_list(self, client: Kernel) -> None: deployment = response.parse() assert_matches_type(DeploymentListResponse, deployment, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_list(self, client: Kernel) -> None: with client.deployments.with_streaming_response.list() as response: @@ -149,9 +149,7 @@ def test_streaming_response_list(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") @parametrize def test_method_follow(self, client: Kernel) -> None: deployment_stream = client.deployments.follow( @@ -159,9 +157,7 @@ def test_method_follow(self, client: Kernel) -> None: ) deployment_stream.response.close() - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") @parametrize def test_method_follow_with_all_params(self, client: Kernel) -> None: deployment_stream = client.deployments.follow( @@ -170,9 +166,7 @@ def test_method_follow_with_all_params(self, client: Kernel) -> None: ) deployment_stream.response.close() - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") @parametrize def test_raw_response_follow(self, client: Kernel) -> None: response = client.deployments.with_raw_response.follow( @@ -183,9 +177,7 @@ def test_raw_response_follow(self, client: Kernel) -> None: stream = response.parse() stream.close() - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") @parametrize def test_streaming_response_follow(self, client: Kernel) -> None: with client.deployments.with_streaming_response.follow( @@ -199,9 +191,7 @@ def test_streaming_response_follow(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") @parametrize def test_path_params_follow(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -215,7 +205,7 @@ class TestAsyncDeployments: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_create(self, async_client: AsyncKernel) -> None: deployment = await async_client.deployments.create( @@ -224,7 +214,7 @@ async def test_method_create(self, async_client: AsyncKernel) -> None: ) assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: deployment = await async_client.deployments.create( @@ -237,7 +227,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> ) assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_create(self, async_client: AsyncKernel) -> None: response = await async_client.deployments.with_raw_response.create( @@ -250,7 +240,7 @@ async def test_raw_response_create(self, async_client: AsyncKernel) -> None: deployment = await response.parse() assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: async with async_client.deployments.with_streaming_response.create( @@ -265,7 +255,7 @@ async def test_streaming_response_create(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_retrieve(self, async_client: AsyncKernel) -> None: deployment = await async_client.deployments.retrieve( @@ -273,7 +263,7 @@ async def test_method_retrieve(self, async_client: AsyncKernel) -> None: ) assert_matches_type(DeploymentRetrieveResponse, deployment, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: response = await async_client.deployments.with_raw_response.retrieve( @@ -285,7 +275,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: deployment = await response.parse() assert_matches_type(DeploymentRetrieveResponse, deployment, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: async with async_client.deployments.with_streaming_response.retrieve( @@ -299,7 +289,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> N assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -307,13 +297,13 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: "", ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_list(self, async_client: AsyncKernel) -> None: deployment = await async_client.deployments.list() assert_matches_type(DeploymentListResponse, deployment, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: deployment = await async_client.deployments.list( @@ -321,7 +311,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N ) assert_matches_type(DeploymentListResponse, deployment, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_list(self, async_client: AsyncKernel) -> None: response = await async_client.deployments.with_raw_response.list() @@ -331,7 +321,7 @@ async def test_raw_response_list(self, async_client: AsyncKernel) -> None: deployment = await response.parse() assert_matches_type(DeploymentListResponse, deployment, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: async with async_client.deployments.with_streaming_response.list() as response: @@ -343,9 +333,7 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") @parametrize async def test_method_follow(self, async_client: AsyncKernel) -> None: deployment_stream = await async_client.deployments.follow( @@ -353,9 +341,7 @@ async def test_method_follow(self, async_client: AsyncKernel) -> None: ) await deployment_stream.response.aclose() - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") @parametrize async def test_method_follow_with_all_params(self, async_client: AsyncKernel) -> None: deployment_stream = await async_client.deployments.follow( @@ -364,9 +350,7 @@ async def test_method_follow_with_all_params(self, async_client: AsyncKernel) -> ) await deployment_stream.response.aclose() - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") @parametrize async def test_raw_response_follow(self, async_client: AsyncKernel) -> None: response = await async_client.deployments.with_raw_response.follow( @@ -377,9 +361,7 @@ async def test_raw_response_follow(self, async_client: AsyncKernel) -> None: stream = await response.parse() await stream.close() - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") @parametrize async def test_streaming_response_follow(self, async_client: AsyncKernel) -> None: async with async_client.deployments.with_streaming_response.follow( @@ -393,9 +375,7 @@ async def test_streaming_response_follow(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") @parametrize async def test_path_params_follow(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): diff --git a/tests/api_resources/test_invocations.py b/tests/api_resources/test_invocations.py index e739e447..852f7f94 100644 --- a/tests/api_resources/test_invocations.py +++ b/tests/api_resources/test_invocations.py @@ -21,7 +21,7 @@ class TestInvocations: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_create(self, client: Kernel) -> None: invocation = client.invocations.create( @@ -31,7 +31,7 @@ def test_method_create(self, client: Kernel) -> None: ) assert_matches_type(InvocationCreateResponse, invocation, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_create_with_all_params(self, client: Kernel) -> None: invocation = client.invocations.create( @@ -43,7 +43,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(InvocationCreateResponse, invocation, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_create(self, client: Kernel) -> None: response = client.invocations.with_raw_response.create( @@ -57,7 +57,7 @@ def test_raw_response_create(self, client: Kernel) -> None: invocation = response.parse() assert_matches_type(InvocationCreateResponse, invocation, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_create(self, client: Kernel) -> None: with client.invocations.with_streaming_response.create( @@ -73,7 +73,7 @@ def test_streaming_response_create(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_retrieve(self, client: Kernel) -> None: invocation = client.invocations.retrieve( @@ -81,7 +81,7 @@ def test_method_retrieve(self, client: Kernel) -> None: ) assert_matches_type(InvocationRetrieveResponse, invocation, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_retrieve(self, client: Kernel) -> None: response = client.invocations.with_raw_response.retrieve( @@ -93,7 +93,7 @@ def test_raw_response_retrieve(self, client: Kernel) -> None: invocation = response.parse() assert_matches_type(InvocationRetrieveResponse, invocation, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_retrieve(self, client: Kernel) -> None: with client.invocations.with_streaming_response.retrieve( @@ -107,7 +107,7 @@ def test_streaming_response_retrieve(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_path_params_retrieve(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -115,7 +115,7 @@ def test_path_params_retrieve(self, client: Kernel) -> None: "", ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_update(self, client: Kernel) -> None: invocation = client.invocations.update( @@ -124,7 +124,7 @@ def test_method_update(self, client: Kernel) -> None: ) assert_matches_type(InvocationUpdateResponse, invocation, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_update_with_all_params(self, client: Kernel) -> None: invocation = client.invocations.update( @@ -134,7 +134,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(InvocationUpdateResponse, invocation, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_update(self, client: Kernel) -> None: response = client.invocations.with_raw_response.update( @@ -147,7 +147,7 @@ def test_raw_response_update(self, client: Kernel) -> None: invocation = response.parse() assert_matches_type(InvocationUpdateResponse, invocation, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_update(self, client: Kernel) -> None: with client.invocations.with_streaming_response.update( @@ -162,7 +162,7 @@ def test_streaming_response_update(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_path_params_update(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -171,7 +171,7 @@ def test_path_params_update(self, client: Kernel) -> None: status="succeeded", ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_delete_browsers(self, client: Kernel) -> None: invocation = client.invocations.delete_browsers( @@ -179,7 +179,7 @@ def test_method_delete_browsers(self, client: Kernel) -> None: ) assert invocation is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_delete_browsers(self, client: Kernel) -> None: response = client.invocations.with_raw_response.delete_browsers( @@ -191,7 +191,7 @@ def test_raw_response_delete_browsers(self, client: Kernel) -> None: invocation = response.parse() assert invocation is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_delete_browsers(self, client: Kernel) -> None: with client.invocations.with_streaming_response.delete_browsers( @@ -205,7 +205,7 @@ def test_streaming_response_delete_browsers(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_path_params_delete_browsers(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -213,9 +213,7 @@ def test_path_params_delete_browsers(self, client: Kernel) -> None: "", ) - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") @parametrize def test_method_follow(self, client: Kernel) -> None: invocation_stream = client.invocations.follow( @@ -223,9 +221,7 @@ def test_method_follow(self, client: Kernel) -> None: ) invocation_stream.response.close() - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") @parametrize def test_raw_response_follow(self, client: Kernel) -> None: response = client.invocations.with_raw_response.follow( @@ -236,9 +232,7 @@ def test_raw_response_follow(self, client: Kernel) -> None: stream = response.parse() stream.close() - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") @parametrize def test_streaming_response_follow(self, client: Kernel) -> None: with client.invocations.with_streaming_response.follow( @@ -252,9 +246,7 @@ def test_streaming_response_follow(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") @parametrize def test_path_params_follow(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -268,7 +260,7 @@ class TestAsyncInvocations: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_create(self, async_client: AsyncKernel) -> None: invocation = await async_client.invocations.create( @@ -278,7 +270,7 @@ async def test_method_create(self, async_client: AsyncKernel) -> None: ) assert_matches_type(InvocationCreateResponse, invocation, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: invocation = await async_client.invocations.create( @@ -290,7 +282,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> ) assert_matches_type(InvocationCreateResponse, invocation, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_create(self, async_client: AsyncKernel) -> None: response = await async_client.invocations.with_raw_response.create( @@ -304,7 +296,7 @@ async def test_raw_response_create(self, async_client: AsyncKernel) -> None: invocation = await response.parse() assert_matches_type(InvocationCreateResponse, invocation, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: async with async_client.invocations.with_streaming_response.create( @@ -320,7 +312,7 @@ async def test_streaming_response_create(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_retrieve(self, async_client: AsyncKernel) -> None: invocation = await async_client.invocations.retrieve( @@ -328,7 +320,7 @@ async def test_method_retrieve(self, async_client: AsyncKernel) -> None: ) assert_matches_type(InvocationRetrieveResponse, invocation, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: response = await async_client.invocations.with_raw_response.retrieve( @@ -340,7 +332,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: invocation = await response.parse() assert_matches_type(InvocationRetrieveResponse, invocation, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: async with async_client.invocations.with_streaming_response.retrieve( @@ -354,7 +346,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> N assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -362,7 +354,7 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: "", ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_update(self, async_client: AsyncKernel) -> None: invocation = await async_client.invocations.update( @@ -371,7 +363,7 @@ async def test_method_update(self, async_client: AsyncKernel) -> None: ) assert_matches_type(InvocationUpdateResponse, invocation, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> None: invocation = await async_client.invocations.update( @@ -381,7 +373,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> ) assert_matches_type(InvocationUpdateResponse, invocation, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_update(self, async_client: AsyncKernel) -> None: response = await async_client.invocations.with_raw_response.update( @@ -394,7 +386,7 @@ async def test_raw_response_update(self, async_client: AsyncKernel) -> None: invocation = await response.parse() assert_matches_type(InvocationUpdateResponse, invocation, path=["response"]) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_update(self, async_client: AsyncKernel) -> None: async with async_client.invocations.with_streaming_response.update( @@ -409,7 +401,7 @@ async def test_streaming_response_update(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_path_params_update(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -418,7 +410,7 @@ async def test_path_params_update(self, async_client: AsyncKernel) -> None: status="succeeded", ) - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_delete_browsers(self, async_client: AsyncKernel) -> None: invocation = await async_client.invocations.delete_browsers( @@ -426,7 +418,7 @@ async def test_method_delete_browsers(self, async_client: AsyncKernel) -> None: ) assert invocation is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_delete_browsers(self, async_client: AsyncKernel) -> None: response = await async_client.invocations.with_raw_response.delete_browsers( @@ -438,7 +430,7 @@ async def test_raw_response_delete_browsers(self, async_client: AsyncKernel) -> invocation = await response.parse() assert invocation is None - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_delete_browsers(self, async_client: AsyncKernel) -> None: async with async_client.invocations.with_streaming_response.delete_browsers( @@ -452,7 +444,7 @@ async def test_streaming_response_delete_browsers(self, async_client: AsyncKerne assert cast(Any, response.is_closed) is True - @pytest.mark.skip() + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_path_params_delete_browsers(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -460,9 +452,7 @@ async def test_path_params_delete_browsers(self, async_client: AsyncKernel) -> N "", ) - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") @parametrize async def test_method_follow(self, async_client: AsyncKernel) -> None: invocation_stream = await async_client.invocations.follow( @@ -470,9 +460,7 @@ async def test_method_follow(self, async_client: AsyncKernel) -> None: ) await invocation_stream.response.aclose() - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") @parametrize async def test_raw_response_follow(self, async_client: AsyncKernel) -> None: response = await async_client.invocations.with_raw_response.follow( @@ -483,9 +471,7 @@ async def test_raw_response_follow(self, async_client: AsyncKernel) -> None: stream = await response.parse() await stream.close() - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") @parametrize async def test_streaming_response_follow(self, async_client: AsyncKernel) -> None: async with async_client.invocations.with_streaming_response.follow( @@ -499,9 +485,7 @@ async def test_streaming_response_follow(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip( - reason="currently no good way to test endpoints with content type text/event-stream, Prism mock server will fail" - ) + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") @parametrize async def test_path_params_follow(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): From c82ef302376fabae04891db49beb97a909ed6d9c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 15 Aug 2025 17:18:48 +0000 Subject: [PATCH 144/448] feat(api): add browser timeouts --- .stats.yml | 4 ++-- src/kernel/types/browser_create_response.py | 8 ++++++-- src/kernel/types/browser_list_response.py | 8 ++++++-- src/kernel/types/browser_retrieve_response.py | 8 ++++++-- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/.stats.yml b/.stats.yml index 86b4afde..f6de080e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 31 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-6f4aab5f0db80d6ce30ef40274eee347cce0a9465e7f1e5077f8f4a085251ddf.yml -openapi_spec_hash: 8e83254243d1620b80a0dc8aa212ee0d +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-b55c3e0424fa7733487139488b9fff00ad8949ff02ee3160ee36b9334e84b134.yml +openapi_spec_hash: 17f36677e3dc0a3aeb419654c8d5cae3 config_hash: f67e4b33b2fb30c1405ee2fff8096320 diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index 7b7b2ab5..145b267d 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -1,6 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import Optional +from datetime import datetime from .._models import BaseModel from .browser_persistence import BrowserPersistence @@ -12,14 +13,17 @@ class BrowserCreateResponse(BaseModel): cdp_ws_url: str """Websocket URL for Chrome DevTools Protocol connections to the browser session""" + created_at: datetime + """When the browser session was created.""" + headless: bool - """Indicates whether the browser session is headless.""" + """Whether the browser session is running in headless mode.""" session_id: str """Unique identifier for the browser session""" stealth: bool - """Indicates whether the browser session is stealth.""" + """Whether the browser session is running in stealth mode.""" timeout_seconds: int """The number of seconds of inactivity before the browser session is terminated.""" diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index 22fe230b..b0157a1f 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -1,6 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import List, Optional +from datetime import datetime from typing_extensions import TypeAlias from .._models import BaseModel @@ -13,14 +14,17 @@ class BrowserListResponseItem(BaseModel): cdp_ws_url: str """Websocket URL for Chrome DevTools Protocol connections to the browser session""" + created_at: datetime + """When the browser session was created.""" + headless: bool - """Indicates whether the browser session is headless.""" + """Whether the browser session is running in headless mode.""" session_id: str """Unique identifier for the browser session""" stealth: bool - """Indicates whether the browser session is stealth.""" + """Whether the browser session is running in stealth mode.""" timeout_seconds: int """The number of seconds of inactivity before the browser session is terminated.""" diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 74084b5f..f0ab7a5f 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -1,6 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import Optional +from datetime import datetime from .._models import BaseModel from .browser_persistence import BrowserPersistence @@ -12,14 +13,17 @@ class BrowserRetrieveResponse(BaseModel): cdp_ws_url: str """Websocket URL for Chrome DevTools Protocol connections to the browser session""" + created_at: datetime + """When the browser session was created.""" + headless: bool - """Indicates whether the browser session is headless.""" + """Whether the browser session is running in headless mode.""" session_id: str """Unique identifier for the browser session""" stealth: bool - """Indicates whether the browser session is stealth.""" + """Whether the browser session is running in stealth mode.""" timeout_seconds: int """The number of seconds of inactivity before the browser session is terminated.""" From 4e94fe3037a9d99d15b870c9606a542257fdd1ad Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 15 Aug 2025 17:35:07 +0000 Subject: [PATCH 145/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 6d78745c..05988747 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.9.0" + ".": "0.9.1" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 49f04c95..ddb4fbb1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.9.0" +version = "0.9.1" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index a21b0438..de748b73 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.9.0" # x-release-please-version +__version__ = "0.9.1" # x-release-please-version From 451dd9d834ca629e3c3e747671fb796976d4f740 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 22 Aug 2025 05:46:38 +0000 Subject: [PATCH 146/448] chore: update github action --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1692944e..fd673562 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: run: ./scripts/lint build: - if: github.repository == 'stainless-sdks/kernel-python' && (github.event_name == 'push' || github.event.pull_request.head.repo.fork) + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork timeout-minutes: 10 name: build permissions: @@ -61,12 +61,14 @@ jobs: run: rye build - name: Get GitHub OIDC Token + if: github.repository == 'stainless-sdks/kernel-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/kernel-python' env: URL: https://pkg.stainless.com/s AUTH: ${{ steps.github-oidc.outputs.github_token }} From 35f7dc7ae10cbeb759b1ef184367f46227d91a5c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 26 Aug 2025 04:30:02 +0000 Subject: [PATCH 147/448] chore(internal): change ci workflow machines --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd673562..795bdad6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: permissions: contents: read id-token: write - runs-on: depot-ubuntu-24.04 + runs-on: ${{ github.repository == 'stainless-sdks/kernel-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - uses: actions/checkout@v4 From 8d381b7ecb32506601ba90cf39b1083499f68c82 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 27 Aug 2025 06:13:12 +0000 Subject: [PATCH 148/448] fix: avoid newer type syntax --- src/kernel/_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kernel/_models.py b/src/kernel/_models.py index b8387ce9..92f7c10b 100644 --- a/src/kernel/_models.py +++ b/src/kernel/_models.py @@ -304,7 +304,7 @@ def model_dump( exclude_none=exclude_none, ) - return cast(dict[str, Any], json_safe(dumped)) if mode == "json" else dumped + return cast("dict[str, Any]", json_safe(dumped)) if mode == "json" else dumped @override def model_dump_json( From ae3672cbb798d480acb30fdb98b5e8cc8d5b8b8b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 27 Aug 2025 06:19:28 +0000 Subject: [PATCH 149/448] chore(internal): update pyright exclude list --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index ddb4fbb1..584c3677 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -148,6 +148,7 @@ exclude = [ "_dev", ".venv", ".nox", + ".git", ] reportImplicitOverride = true From ce198a7821aa93877986f8df1a939d356b0a90cd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 27 Aug 2025 17:38:43 +0000 Subject: [PATCH 150/448] feat(api): new process, fs, and log endpoints New endpoints for executing processes on browser instances, uploading / downloading whole directories as zip files, and streaming any log file on a browser instance --- .stats.yml | 8 +- api.md | 33 + src/kernel/resources/browsers/__init__.py | 28 + src/kernel/resources/browsers/browsers.py | 64 ++ src/kernel/resources/browsers/fs/fs.py | 319 +++++++- src/kernel/resources/browsers/logs.py | 214 +++++ src/kernel/resources/browsers/process.py | 742 ++++++++++++++++++ src/kernel/types/browsers/__init__.py | 14 + .../browsers/f_download_dir_zip_params.py | 12 + src/kernel/types/browsers/f_upload_params.py | 21 + .../types/browsers/f_upload_zip_params.py | 16 + .../types/browsers/log_stream_params.py | 19 + .../types/browsers/process_exec_params.py | 31 + .../types/browsers/process_exec_response.py | 21 + .../types/browsers/process_kill_params.py | 14 + .../types/browsers/process_kill_response.py | 10 + .../types/browsers/process_spawn_params.py | 31 + .../types/browsers/process_spawn_response.py | 19 + .../types/browsers/process_status_response.py | 22 + .../types/browsers/process_stdin_params.py | 14 + .../types/browsers/process_stdin_response.py | 12 + .../process_stdout_stream_response.py | 22 + tests/api_resources/browsers/test_fs.py | 340 ++++++++ tests/api_resources/browsers/test_logs.py | 136 ++++ tests/api_resources/browsers/test_process.py | 708 +++++++++++++++++ 25 files changed, 2864 insertions(+), 6 deletions(-) create mode 100644 src/kernel/resources/browsers/logs.py create mode 100644 src/kernel/resources/browsers/process.py create mode 100644 src/kernel/types/browsers/f_download_dir_zip_params.py create mode 100644 src/kernel/types/browsers/f_upload_params.py create mode 100644 src/kernel/types/browsers/f_upload_zip_params.py create mode 100644 src/kernel/types/browsers/log_stream_params.py create mode 100644 src/kernel/types/browsers/process_exec_params.py create mode 100644 src/kernel/types/browsers/process_exec_response.py create mode 100644 src/kernel/types/browsers/process_kill_params.py create mode 100644 src/kernel/types/browsers/process_kill_response.py create mode 100644 src/kernel/types/browsers/process_spawn_params.py create mode 100644 src/kernel/types/browsers/process_spawn_response.py create mode 100644 src/kernel/types/browsers/process_status_response.py create mode 100644 src/kernel/types/browsers/process_stdin_params.py create mode 100644 src/kernel/types/browsers/process_stdin_response.py create mode 100644 src/kernel/types/browsers/process_stdout_stream_response.py create mode 100644 tests/api_resources/browsers/test_logs.py create mode 100644 tests/api_resources/browsers/test_process.py diff --git a/.stats.yml b/.stats.yml index f6de080e..b791078d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 31 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-b55c3e0424fa7733487139488b9fff00ad8949ff02ee3160ee36b9334e84b134.yml -openapi_spec_hash: 17f36677e3dc0a3aeb419654c8d5cae3 -config_hash: f67e4b33b2fb30c1405ee2fff8096320 +configured_endpoints: 41 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-a7c1df5070fe59642d7a1f168aa902a468227752bfc930cbf38930f7c205dbb6.yml +openapi_spec_hash: eab65e39aef4f0a0952b82adeecf6b5b +config_hash: 5de78bc29ac060562575cb54bb26826c diff --git a/api.md b/api.md index f03855dc..7e07a7b0 100644 --- a/api.md +++ b/api.md @@ -108,11 +108,14 @@ Methods: - client.browsers.fs.create_directory(id, \*\*params) -> None - client.browsers.fs.delete_directory(id, \*\*params) -> None - client.browsers.fs.delete_file(id, \*\*params) -> None +- client.browsers.fs.download_dir_zip(id, \*\*params) -> BinaryAPIResponse - client.browsers.fs.file_info(id, \*\*params) -> FFileInfoResponse - client.browsers.fs.list_files(id, \*\*params) -> FListFilesResponse - client.browsers.fs.move(id, \*\*params) -> None - client.browsers.fs.read_file(id, \*\*params) -> BinaryAPIResponse - client.browsers.fs.set_file_permissions(id, \*\*params) -> None +- client.browsers.fs.upload(id, \*\*params) -> None +- client.browsers.fs.upload_zip(id, \*\*params) -> None - client.browsers.fs.write_file(id, contents, \*\*params) -> None ### Watch @@ -128,3 +131,33 @@ Methods: - client.browsers.fs.watch.events(watch_id, \*, id) -> WatchEventsResponse - client.browsers.fs.watch.start(id, \*\*params) -> WatchStartResponse - client.browsers.fs.watch.stop(watch_id, \*, id) -> None + +## Process + +Types: + +```python +from kernel.types.browsers import ( + ProcessExecResponse, + ProcessKillResponse, + ProcessSpawnResponse, + ProcessStatusResponse, + ProcessStdinResponse, + ProcessStdoutStreamResponse, +) +``` + +Methods: + +- client.browsers.process.exec(id, \*\*params) -> ProcessExecResponse +- client.browsers.process.kill(process_id, \*, id, \*\*params) -> ProcessKillResponse +- client.browsers.process.spawn(id, \*\*params) -> ProcessSpawnResponse +- client.browsers.process.status(process_id, \*, id) -> ProcessStatusResponse +- client.browsers.process.stdin(process_id, \*, id, \*\*params) -> ProcessStdinResponse +- client.browsers.process.stdout_stream(process_id, \*, id) -> ProcessStdoutStreamResponse + +## Logs + +Methods: + +- client.browsers.logs.stream(id, \*\*params) -> LogEvent diff --git a/src/kernel/resources/browsers/__init__.py b/src/kernel/resources/browsers/__init__.py index 41452e9d..97c987e4 100644 --- a/src/kernel/resources/browsers/__init__.py +++ b/src/kernel/resources/browsers/__init__.py @@ -8,6 +8,22 @@ FsResourceWithStreamingResponse, AsyncFsResourceWithStreamingResponse, ) +from .logs import ( + LogsResource, + AsyncLogsResource, + LogsResourceWithRawResponse, + AsyncLogsResourceWithRawResponse, + LogsResourceWithStreamingResponse, + AsyncLogsResourceWithStreamingResponse, +) +from .process import ( + ProcessResource, + AsyncProcessResource, + ProcessResourceWithRawResponse, + AsyncProcessResourceWithRawResponse, + ProcessResourceWithStreamingResponse, + AsyncProcessResourceWithStreamingResponse, +) from .replays import ( ReplaysResource, AsyncReplaysResource, @@ -38,6 +54,18 @@ "AsyncFsResourceWithRawResponse", "FsResourceWithStreamingResponse", "AsyncFsResourceWithStreamingResponse", + "ProcessResource", + "AsyncProcessResource", + "ProcessResourceWithRawResponse", + "AsyncProcessResourceWithRawResponse", + "ProcessResourceWithStreamingResponse", + "AsyncProcessResourceWithStreamingResponse", + "LogsResource", + "AsyncLogsResource", + "LogsResourceWithRawResponse", + "AsyncLogsResourceWithRawResponse", + "LogsResourceWithStreamingResponse", + "AsyncLogsResourceWithStreamingResponse", "BrowsersResource", "AsyncBrowsersResource", "BrowsersResourceWithRawResponse", diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 559e0948..80afc60d 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -4,6 +4,14 @@ import httpx +from .logs import ( + LogsResource, + AsyncLogsResource, + LogsResourceWithRawResponse, + AsyncLogsResourceWithRawResponse, + LogsResourceWithStreamingResponse, + AsyncLogsResourceWithStreamingResponse, +) from .fs.fs import ( FsResource, AsyncFsResource, @@ -13,6 +21,14 @@ AsyncFsResourceWithStreamingResponse, ) from ...types import browser_create_params, browser_delete_params +from .process import ( + ProcessResource, + AsyncProcessResource, + ProcessResourceWithRawResponse, + AsyncProcessResourceWithRawResponse, + ProcessResourceWithStreamingResponse, + AsyncProcessResourceWithStreamingResponse, +) from .replays import ( ReplaysResource, AsyncReplaysResource, @@ -49,6 +65,14 @@ def replays(self) -> ReplaysResource: def fs(self) -> FsResource: return FsResource(self._client) + @cached_property + def process(self) -> ProcessResource: + return ProcessResource(self._client) + + @cached_property + def logs(self) -> LogsResource: + return LogsResource(self._client) + @cached_property def with_raw_response(self) -> BrowsersResourceWithRawResponse: """ @@ -261,6 +285,14 @@ def replays(self) -> AsyncReplaysResource: def fs(self) -> AsyncFsResource: return AsyncFsResource(self._client) + @cached_property + def process(self) -> AsyncProcessResource: + return AsyncProcessResource(self._client) + + @cached_property + def logs(self) -> AsyncLogsResource: + return AsyncLogsResource(self._client) + @cached_property def with_raw_response(self) -> AsyncBrowsersResourceWithRawResponse: """ @@ -494,6 +526,14 @@ def replays(self) -> ReplaysResourceWithRawResponse: def fs(self) -> FsResourceWithRawResponse: return FsResourceWithRawResponse(self._browsers.fs) + @cached_property + def process(self) -> ProcessResourceWithRawResponse: + return ProcessResourceWithRawResponse(self._browsers.process) + + @cached_property + def logs(self) -> LogsResourceWithRawResponse: + return LogsResourceWithRawResponse(self._browsers.logs) + class AsyncBrowsersResourceWithRawResponse: def __init__(self, browsers: AsyncBrowsersResource) -> None: @@ -523,6 +563,14 @@ def replays(self) -> AsyncReplaysResourceWithRawResponse: def fs(self) -> AsyncFsResourceWithRawResponse: return AsyncFsResourceWithRawResponse(self._browsers.fs) + @cached_property + def process(self) -> AsyncProcessResourceWithRawResponse: + return AsyncProcessResourceWithRawResponse(self._browsers.process) + + @cached_property + def logs(self) -> AsyncLogsResourceWithRawResponse: + return AsyncLogsResourceWithRawResponse(self._browsers.logs) + class BrowsersResourceWithStreamingResponse: def __init__(self, browsers: BrowsersResource) -> None: @@ -552,6 +600,14 @@ def replays(self) -> ReplaysResourceWithStreamingResponse: def fs(self) -> FsResourceWithStreamingResponse: return FsResourceWithStreamingResponse(self._browsers.fs) + @cached_property + def process(self) -> ProcessResourceWithStreamingResponse: + return ProcessResourceWithStreamingResponse(self._browsers.process) + + @cached_property + def logs(self) -> LogsResourceWithStreamingResponse: + return LogsResourceWithStreamingResponse(self._browsers.logs) + class AsyncBrowsersResourceWithStreamingResponse: def __init__(self, browsers: AsyncBrowsersResource) -> None: @@ -580,3 +636,11 @@ def replays(self) -> AsyncReplaysResourceWithStreamingResponse: @cached_property def fs(self) -> AsyncFsResourceWithStreamingResponse: return AsyncFsResourceWithStreamingResponse(self._browsers.fs) + + @cached_property + def process(self) -> AsyncProcessResourceWithStreamingResponse: + return AsyncProcessResourceWithStreamingResponse(self._browsers.process) + + @cached_property + def logs(self) -> AsyncLogsResourceWithStreamingResponse: + return AsyncLogsResourceWithStreamingResponse(self._browsers.logs) diff --git a/src/kernel/resources/browsers/fs/fs.py b/src/kernel/resources/browsers/fs/fs.py index 3563c7cb..cb7b3010 100644 --- a/src/kernel/resources/browsers/fs/fs.py +++ b/src/kernel/resources/browsers/fs/fs.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Mapping, Iterable, cast + import httpx from .watch import ( @@ -13,8 +15,8 @@ AsyncWatchResourceWithStreamingResponse, ) from ...._files import read_file_content, async_read_file_content -from ...._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven, FileContent -from ...._utils import maybe_transform, async_maybe_transform +from ...._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven, FileTypes, FileContent +from ...._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform from ...._compat import cached_property from ...._resource import SyncAPIResource, AsyncAPIResource from ...._response import ( @@ -34,13 +36,16 @@ from ...._base_client import make_request_options from ....types.browsers import ( f_move_params, + f_upload_params, f_file_info_params, f_read_file_params, f_list_files_params, + f_upload_zip_params, f_write_file_params, f_delete_file_params, f_create_directory_params, f_delete_directory_params, + f_download_dir_zip_params, f_set_file_permissions_params, ) from ....types.browsers.f_file_info_response import FFileInfoResponse @@ -196,6 +201,47 @@ def delete_file( cast_to=NoneType, ) + def download_dir_zip( + self, + id: str, + *, + path: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BinaryAPIResponse: + """ + Returns a ZIP file containing the contents of the specified directory. + + Args: + path: Absolute directory path to archive and download. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "application/zip", **(extra_headers or {})} + return self._get( + f"/browsers/{id}/fs/download_dir_zip", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform({"path": path}, f_download_dir_zip_params.FDownloadDirZipParams), + ), + cast_to=BinaryAPIResponse, + ) + def file_info( self, id: str, @@ -419,6 +465,100 @@ def set_file_permissions( cast_to=NoneType, ) + def upload( + self, + id: str, + *, + files: Iterable[f_upload_params.File], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via 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: + """ + Allows uploading single or multiple files to the remote filesystem. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + body = deepcopy_minimal({"files": files}) + extracted_files = extract_files(cast(Mapping[str, object], body), paths=[["files", "", "file"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers["Content-Type"] = "multipart/form-data" + return self._post( + f"/browsers/{id}/fs/upload", + body=maybe_transform(body, f_upload_params.FUploadParams), + files=extracted_files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + def upload_zip( + self, + id: str, + *, + dest_path: str, + zip_file: FileTypes, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """ + Upload a zip file and extract its contents to the specified destination path. + + Args: + dest_path: Absolute destination directory to extract the archive to. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + body = deepcopy_minimal( + { + "dest_path": dest_path, + "zip_file": zip_file, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["zip_file"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers["Content-Type"] = "multipart/form-data" + return self._post( + f"/browsers/{id}/fs/upload_zip", + body=maybe_transform(body, f_upload_zip_params.FUploadZipParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + def write_file( self, id: str, @@ -620,6 +760,47 @@ async def delete_file( cast_to=NoneType, ) + async def download_dir_zip( + self, + id: str, + *, + path: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncBinaryAPIResponse: + """ + Returns a ZIP file containing the contents of the specified directory. + + Args: + path: Absolute directory path to archive and download. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "application/zip", **(extra_headers or {})} + return await self._get( + f"/browsers/{id}/fs/download_dir_zip", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform({"path": path}, f_download_dir_zip_params.FDownloadDirZipParams), + ), + cast_to=AsyncBinaryAPIResponse, + ) + async def file_info( self, id: str, @@ -843,6 +1024,100 @@ async def set_file_permissions( cast_to=NoneType, ) + async def upload( + self, + id: str, + *, + files: Iterable[f_upload_params.File], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via 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: + """ + Allows uploading single or multiple files to the remote filesystem. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + body = deepcopy_minimal({"files": files}) + extracted_files = extract_files(cast(Mapping[str, object], body), paths=[["files", "", "file"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers["Content-Type"] = "multipart/form-data" + return await self._post( + f"/browsers/{id}/fs/upload", + body=await async_maybe_transform(body, f_upload_params.FUploadParams), + files=extracted_files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + async def upload_zip( + self, + id: str, + *, + dest_path: str, + zip_file: FileTypes, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """ + Upload a zip file and extract its contents to the specified destination path. + + Args: + dest_path: Absolute destination directory to extract the archive to. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + body = deepcopy_minimal( + { + "dest_path": dest_path, + "zip_file": zip_file, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["zip_file"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers["Content-Type"] = "multipart/form-data" + return await self._post( + f"/browsers/{id}/fs/upload_zip", + body=await async_maybe_transform(body, f_upload_zip_params.FUploadZipParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + async def write_file( self, id: str, @@ -910,6 +1185,10 @@ def __init__(self, fs: FsResource) -> None: self.delete_file = to_raw_response_wrapper( fs.delete_file, ) + self.download_dir_zip = to_custom_raw_response_wrapper( + fs.download_dir_zip, + BinaryAPIResponse, + ) self.file_info = to_raw_response_wrapper( fs.file_info, ) @@ -926,6 +1205,12 @@ def __init__(self, fs: FsResource) -> None: self.set_file_permissions = to_raw_response_wrapper( fs.set_file_permissions, ) + self.upload = to_raw_response_wrapper( + fs.upload, + ) + self.upload_zip = to_raw_response_wrapper( + fs.upload_zip, + ) self.write_file = to_raw_response_wrapper( fs.write_file, ) @@ -948,6 +1233,10 @@ def __init__(self, fs: AsyncFsResource) -> None: self.delete_file = async_to_raw_response_wrapper( fs.delete_file, ) + self.download_dir_zip = async_to_custom_raw_response_wrapper( + fs.download_dir_zip, + AsyncBinaryAPIResponse, + ) self.file_info = async_to_raw_response_wrapper( fs.file_info, ) @@ -964,6 +1253,12 @@ def __init__(self, fs: AsyncFsResource) -> None: self.set_file_permissions = async_to_raw_response_wrapper( fs.set_file_permissions, ) + self.upload = async_to_raw_response_wrapper( + fs.upload, + ) + self.upload_zip = async_to_raw_response_wrapper( + fs.upload_zip, + ) self.write_file = async_to_raw_response_wrapper( fs.write_file, ) @@ -986,6 +1281,10 @@ def __init__(self, fs: FsResource) -> None: self.delete_file = to_streamed_response_wrapper( fs.delete_file, ) + self.download_dir_zip = to_custom_streamed_response_wrapper( + fs.download_dir_zip, + StreamedBinaryAPIResponse, + ) self.file_info = to_streamed_response_wrapper( fs.file_info, ) @@ -1002,6 +1301,12 @@ def __init__(self, fs: FsResource) -> None: self.set_file_permissions = to_streamed_response_wrapper( fs.set_file_permissions, ) + self.upload = to_streamed_response_wrapper( + fs.upload, + ) + self.upload_zip = to_streamed_response_wrapper( + fs.upload_zip, + ) self.write_file = to_streamed_response_wrapper( fs.write_file, ) @@ -1024,6 +1329,10 @@ def __init__(self, fs: AsyncFsResource) -> None: self.delete_file = async_to_streamed_response_wrapper( fs.delete_file, ) + self.download_dir_zip = async_to_custom_streamed_response_wrapper( + fs.download_dir_zip, + AsyncStreamedBinaryAPIResponse, + ) self.file_info = async_to_streamed_response_wrapper( fs.file_info, ) @@ -1040,6 +1349,12 @@ def __init__(self, fs: AsyncFsResource) -> None: self.set_file_permissions = async_to_streamed_response_wrapper( fs.set_file_permissions, ) + self.upload = async_to_streamed_response_wrapper( + fs.upload, + ) + self.upload_zip = async_to_streamed_response_wrapper( + fs.upload_zip, + ) self.write_file = async_to_streamed_response_wrapper( fs.write_file, ) diff --git a/src/kernel/resources/browsers/logs.py b/src/kernel/resources/browsers/logs.py new file mode 100644 index 00000000..fbbe14a5 --- /dev/null +++ b/src/kernel/resources/browsers/logs.py @@ -0,0 +1,214 @@ +# 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 ..._streaming import Stream, AsyncStream +from ..._base_client import make_request_options +from ...types.browsers import log_stream_params +from ...types.shared.log_event import LogEvent + +__all__ = ["LogsResource", "AsyncLogsResource"] + + +class LogsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> LogsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return LogsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> LogsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return LogsResourceWithStreamingResponse(self) + + def stream( + self, + id: str, + *, + source: Literal["path", "supervisor"], + follow: bool | NotGiven = NOT_GIVEN, + path: str | NotGiven = NOT_GIVEN, + supervisor_process: 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, + ) -> Stream[LogEvent]: + """ + Stream log files on the browser instance via SSE + + Args: + path: only required if source is path + + supervisor_process: only required if source is supervisor + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} + return self._get( + f"/browsers/{id}/logs/stream", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "source": source, + "follow": follow, + "path": path, + "supervisor_process": supervisor_process, + }, + log_stream_params.LogStreamParams, + ), + ), + cast_to=LogEvent, + stream=True, + stream_cls=Stream[LogEvent], + ) + + +class AsyncLogsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncLogsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncLogsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncLogsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return AsyncLogsResourceWithStreamingResponse(self) + + async def stream( + self, + id: str, + *, + source: Literal["path", "supervisor"], + follow: bool | NotGiven = NOT_GIVEN, + path: str | NotGiven = NOT_GIVEN, + supervisor_process: 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, + ) -> AsyncStream[LogEvent]: + """ + Stream log files on the browser instance via SSE + + Args: + path: only required if source is path + + supervisor_process: only required if source is supervisor + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} + return await self._get( + f"/browsers/{id}/logs/stream", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "source": source, + "follow": follow, + "path": path, + "supervisor_process": supervisor_process, + }, + log_stream_params.LogStreamParams, + ), + ), + cast_to=LogEvent, + stream=True, + stream_cls=AsyncStream[LogEvent], + ) + + +class LogsResourceWithRawResponse: + def __init__(self, logs: LogsResource) -> None: + self._logs = logs + + self.stream = to_raw_response_wrapper( + logs.stream, + ) + + +class AsyncLogsResourceWithRawResponse: + def __init__(self, logs: AsyncLogsResource) -> None: + self._logs = logs + + self.stream = async_to_raw_response_wrapper( + logs.stream, + ) + + +class LogsResourceWithStreamingResponse: + def __init__(self, logs: LogsResource) -> None: + self._logs = logs + + self.stream = to_streamed_response_wrapper( + logs.stream, + ) + + +class AsyncLogsResourceWithStreamingResponse: + def __init__(self, logs: AsyncLogsResource) -> None: + self._logs = logs + + self.stream = async_to_streamed_response_wrapper( + logs.stream, + ) diff --git a/src/kernel/resources/browsers/process.py b/src/kernel/resources/browsers/process.py new file mode 100644 index 00000000..d3f5eca5 --- /dev/null +++ b/src/kernel/resources/browsers/process.py @@ -0,0 +1,742 @@ +# 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 + +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 ..._streaming import Stream, AsyncStream +from ..._base_client import make_request_options +from ...types.browsers import process_exec_params, process_kill_params, process_spawn_params, process_stdin_params +from ...types.browsers.process_exec_response import ProcessExecResponse +from ...types.browsers.process_kill_response import ProcessKillResponse +from ...types.browsers.process_spawn_response import ProcessSpawnResponse +from ...types.browsers.process_stdin_response import ProcessStdinResponse +from ...types.browsers.process_status_response import ProcessStatusResponse +from ...types.browsers.process_stdout_stream_response import ProcessStdoutStreamResponse + +__all__ = ["ProcessResource", "AsyncProcessResource"] + + +class ProcessResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> ProcessResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return ProcessResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ProcessResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return ProcessResourceWithStreamingResponse(self) + + def exec( + self, + id: str, + *, + command: str, + args: List[str] | NotGiven = NOT_GIVEN, + as_root: bool | NotGiven = NOT_GIVEN, + as_user: Optional[str] | NotGiven = NOT_GIVEN, + cwd: Optional[str] | NotGiven = NOT_GIVEN, + env: Dict[str, str] | NotGiven = NOT_GIVEN, + timeout_sec: Optional[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, + ) -> ProcessExecResponse: + """ + Execute a command synchronously + + Args: + command: Executable or shell command to run. + + args: Command arguments. + + as_root: Run the process with root privileges. + + as_user: Run the process as this user. + + cwd: Working directory (absolute path) to run the command in. + + env: Environment variables to set for the process. + + timeout_sec: Maximum execution time in seconds. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/browsers/{id}/process/exec", + body=maybe_transform( + { + "command": command, + "args": args, + "as_root": as_root, + "as_user": as_user, + "cwd": cwd, + "env": env, + "timeout_sec": timeout_sec, + }, + process_exec_params.ProcessExecParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProcessExecResponse, + ) + + def kill( + self, + process_id: str, + *, + id: str, + signal: Literal["TERM", "KILL", "INT", "HUP"], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via 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, + ) -> ProcessKillResponse: + """ + Send signal to process + + Args: + signal: Signal to send. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not process_id: + raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}") + return self._post( + f"/browsers/{id}/process/{process_id}/kill", + body=maybe_transform({"signal": signal}, process_kill_params.ProcessKillParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProcessKillResponse, + ) + + def spawn( + self, + id: str, + *, + command: str, + args: List[str] | NotGiven = NOT_GIVEN, + as_root: bool | NotGiven = NOT_GIVEN, + as_user: Optional[str] | NotGiven = NOT_GIVEN, + cwd: Optional[str] | NotGiven = NOT_GIVEN, + env: Dict[str, str] | NotGiven = NOT_GIVEN, + timeout_sec: Optional[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, + ) -> ProcessSpawnResponse: + """ + Execute a command asynchronously + + Args: + command: Executable or shell command to run. + + args: Command arguments. + + as_root: Run the process with root privileges. + + as_user: Run the process as this user. + + cwd: Working directory (absolute path) to run the command in. + + env: Environment variables to set for the process. + + timeout_sec: Maximum execution time in seconds. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/browsers/{id}/process/spawn", + body=maybe_transform( + { + "command": command, + "args": args, + "as_root": as_root, + "as_user": as_user, + "cwd": cwd, + "env": env, + "timeout_sec": timeout_sec, + }, + process_spawn_params.ProcessSpawnParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProcessSpawnResponse, + ) + + def status( + self, + process_id: str, + *, + id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ProcessStatusResponse: + """ + Get process status + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not process_id: + raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}") + return self._get( + f"/browsers/{id}/process/{process_id}/status", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProcessStatusResponse, + ) + + def stdin( + self, + process_id: str, + *, + id: str, + data_b64: 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, + ) -> ProcessStdinResponse: + """ + Write to process stdin + + Args: + data_b64: Base64-encoded data to write. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not process_id: + raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}") + return self._post( + f"/browsers/{id}/process/{process_id}/stdin", + body=maybe_transform({"data_b64": data_b64}, process_stdin_params.ProcessStdinParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProcessStdinResponse, + ) + + def stdout_stream( + self, + process_id: str, + *, + id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Stream[ProcessStdoutStreamResponse]: + """ + Stream process stdout via SSE + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not process_id: + raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}") + extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} + return self._get( + f"/browsers/{id}/process/{process_id}/stdout/stream", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProcessStdoutStreamResponse, + stream=True, + stream_cls=Stream[ProcessStdoutStreamResponse], + ) + + +class AsyncProcessResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncProcessResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncProcessResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncProcessResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return AsyncProcessResourceWithStreamingResponse(self) + + async def exec( + self, + id: str, + *, + command: str, + args: List[str] | NotGiven = NOT_GIVEN, + as_root: bool | NotGiven = NOT_GIVEN, + as_user: Optional[str] | NotGiven = NOT_GIVEN, + cwd: Optional[str] | NotGiven = NOT_GIVEN, + env: Dict[str, str] | NotGiven = NOT_GIVEN, + timeout_sec: Optional[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, + ) -> ProcessExecResponse: + """ + Execute a command synchronously + + Args: + command: Executable or shell command to run. + + args: Command arguments. + + as_root: Run the process with root privileges. + + as_user: Run the process as this user. + + cwd: Working directory (absolute path) to run the command in. + + env: Environment variables to set for the process. + + timeout_sec: Maximum execution time in seconds. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/browsers/{id}/process/exec", + body=await async_maybe_transform( + { + "command": command, + "args": args, + "as_root": as_root, + "as_user": as_user, + "cwd": cwd, + "env": env, + "timeout_sec": timeout_sec, + }, + process_exec_params.ProcessExecParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProcessExecResponse, + ) + + async def kill( + self, + process_id: str, + *, + id: str, + signal: Literal["TERM", "KILL", "INT", "HUP"], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via 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, + ) -> ProcessKillResponse: + """ + Send signal to process + + Args: + signal: Signal to send. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not process_id: + raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}") + return await self._post( + f"/browsers/{id}/process/{process_id}/kill", + body=await async_maybe_transform({"signal": signal}, process_kill_params.ProcessKillParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProcessKillResponse, + ) + + async def spawn( + self, + id: str, + *, + command: str, + args: List[str] | NotGiven = NOT_GIVEN, + as_root: bool | NotGiven = NOT_GIVEN, + as_user: Optional[str] | NotGiven = NOT_GIVEN, + cwd: Optional[str] | NotGiven = NOT_GIVEN, + env: Dict[str, str] | NotGiven = NOT_GIVEN, + timeout_sec: Optional[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, + ) -> ProcessSpawnResponse: + """ + Execute a command asynchronously + + Args: + command: Executable or shell command to run. + + args: Command arguments. + + as_root: Run the process with root privileges. + + as_user: Run the process as this user. + + cwd: Working directory (absolute path) to run the command in. + + env: Environment variables to set for the process. + + timeout_sec: Maximum execution time in seconds. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/browsers/{id}/process/spawn", + body=await async_maybe_transform( + { + "command": command, + "args": args, + "as_root": as_root, + "as_user": as_user, + "cwd": cwd, + "env": env, + "timeout_sec": timeout_sec, + }, + process_spawn_params.ProcessSpawnParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProcessSpawnResponse, + ) + + async def status( + self, + process_id: str, + *, + id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ProcessStatusResponse: + """ + Get process status + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not process_id: + raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}") + return await self._get( + f"/browsers/{id}/process/{process_id}/status", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProcessStatusResponse, + ) + + async def stdin( + self, + process_id: str, + *, + id: str, + data_b64: 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, + ) -> ProcessStdinResponse: + """ + Write to process stdin + + Args: + data_b64: Base64-encoded data to write. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not process_id: + raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}") + return await self._post( + f"/browsers/{id}/process/{process_id}/stdin", + body=await async_maybe_transform({"data_b64": data_b64}, process_stdin_params.ProcessStdinParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProcessStdinResponse, + ) + + async def stdout_stream( + self, + process_id: str, + *, + id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncStream[ProcessStdoutStreamResponse]: + """ + Stream process stdout via SSE + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not process_id: + raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}") + extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} + return await self._get( + f"/browsers/{id}/process/{process_id}/stdout/stream", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProcessStdoutStreamResponse, + stream=True, + stream_cls=AsyncStream[ProcessStdoutStreamResponse], + ) + + +class ProcessResourceWithRawResponse: + def __init__(self, process: ProcessResource) -> None: + self._process = process + + self.exec = to_raw_response_wrapper( + process.exec, + ) + self.kill = to_raw_response_wrapper( + process.kill, + ) + self.spawn = to_raw_response_wrapper( + process.spawn, + ) + self.status = to_raw_response_wrapper( + process.status, + ) + self.stdin = to_raw_response_wrapper( + process.stdin, + ) + self.stdout_stream = to_raw_response_wrapper( + process.stdout_stream, + ) + + +class AsyncProcessResourceWithRawResponse: + def __init__(self, process: AsyncProcessResource) -> None: + self._process = process + + self.exec = async_to_raw_response_wrapper( + process.exec, + ) + self.kill = async_to_raw_response_wrapper( + process.kill, + ) + self.spawn = async_to_raw_response_wrapper( + process.spawn, + ) + self.status = async_to_raw_response_wrapper( + process.status, + ) + self.stdin = async_to_raw_response_wrapper( + process.stdin, + ) + self.stdout_stream = async_to_raw_response_wrapper( + process.stdout_stream, + ) + + +class ProcessResourceWithStreamingResponse: + def __init__(self, process: ProcessResource) -> None: + self._process = process + + self.exec = to_streamed_response_wrapper( + process.exec, + ) + self.kill = to_streamed_response_wrapper( + process.kill, + ) + self.spawn = to_streamed_response_wrapper( + process.spawn, + ) + self.status = to_streamed_response_wrapper( + process.status, + ) + self.stdin = to_streamed_response_wrapper( + process.stdin, + ) + self.stdout_stream = to_streamed_response_wrapper( + process.stdout_stream, + ) + + +class AsyncProcessResourceWithStreamingResponse: + def __init__(self, process: AsyncProcessResource) -> None: + self._process = process + + self.exec = async_to_streamed_response_wrapper( + process.exec, + ) + self.kill = async_to_streamed_response_wrapper( + process.kill, + ) + self.spawn = async_to_streamed_response_wrapper( + process.spawn, + ) + self.status = async_to_streamed_response_wrapper( + process.status, + ) + self.stdin = async_to_streamed_response_wrapper( + process.stdin, + ) + self.stdout_stream = async_to_streamed_response_wrapper( + process.stdout_stream, + ) diff --git a/src/kernel/types/browsers/__init__.py b/src/kernel/types/browsers/__init__.py index c4e80a89..d0b6b383 100644 --- a/src/kernel/types/browsers/__init__.py +++ b/src/kernel/types/browsers/__init__.py @@ -3,16 +3,30 @@ from __future__ import annotations from .f_move_params import FMoveParams as FMoveParams +from .f_upload_params import FUploadParams as FUploadParams +from .log_stream_params import LogStreamParams as LogStreamParams from .f_file_info_params import FFileInfoParams as FFileInfoParams from .f_read_file_params import FReadFileParams as FReadFileParams from .f_list_files_params import FListFilesParams as FListFilesParams +from .f_upload_zip_params import FUploadZipParams as FUploadZipParams from .f_write_file_params import FWriteFileParams as FWriteFileParams +from .process_exec_params import ProcessExecParams as ProcessExecParams +from .process_kill_params import ProcessKillParams as ProcessKillParams from .replay_start_params import ReplayStartParams as ReplayStartParams from .f_delete_file_params import FDeleteFileParams as FDeleteFileParams from .f_file_info_response import FFileInfoResponse as FFileInfoResponse +from .process_spawn_params import ProcessSpawnParams as ProcessSpawnParams +from .process_stdin_params import ProcessStdinParams as ProcessStdinParams from .replay_list_response import ReplayListResponse as ReplayListResponse from .f_list_files_response import FListFilesResponse as FListFilesResponse +from .process_exec_response import ProcessExecResponse as ProcessExecResponse +from .process_kill_response import ProcessKillResponse as ProcessKillResponse from .replay_start_response import ReplayStartResponse as ReplayStartResponse +from .process_spawn_response import ProcessSpawnResponse as ProcessSpawnResponse +from .process_stdin_response import ProcessStdinResponse as ProcessStdinResponse +from .process_status_response import ProcessStatusResponse as ProcessStatusResponse from .f_create_directory_params import FCreateDirectoryParams as FCreateDirectoryParams from .f_delete_directory_params import FDeleteDirectoryParams as FDeleteDirectoryParams +from .f_download_dir_zip_params import FDownloadDirZipParams as FDownloadDirZipParams from .f_set_file_permissions_params import FSetFilePermissionsParams as FSetFilePermissionsParams +from .process_stdout_stream_response import ProcessStdoutStreamResponse as ProcessStdoutStreamResponse diff --git a/src/kernel/types/browsers/f_download_dir_zip_params.py b/src/kernel/types/browsers/f_download_dir_zip_params.py new file mode 100644 index 00000000..88212c64 --- /dev/null +++ b/src/kernel/types/browsers/f_download_dir_zip_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["FDownloadDirZipParams"] + + +class FDownloadDirZipParams(TypedDict, total=False): + path: Required[str] + """Absolute directory path to archive and download.""" diff --git a/src/kernel/types/browsers/f_upload_params.py b/src/kernel/types/browsers/f_upload_params.py new file mode 100644 index 00000000..8f6534bd --- /dev/null +++ b/src/kernel/types/browsers/f_upload_params.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Iterable +from typing_extensions import Required, TypedDict + +from ..._types import FileTypes + +__all__ = ["FUploadParams", "File"] + + +class FUploadParams(TypedDict, total=False): + files: Required[Iterable[File]] + + +class File(TypedDict, total=False): + dest_path: Required[str] + """Absolute destination path to write the file.""" + + file: Required[FileTypes] diff --git a/src/kernel/types/browsers/f_upload_zip_params.py b/src/kernel/types/browsers/f_upload_zip_params.py new file mode 100644 index 00000000..4646e05f --- /dev/null +++ b/src/kernel/types/browsers/f_upload_zip_params.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +from ..._types import FileTypes + +__all__ = ["FUploadZipParams"] + + +class FUploadZipParams(TypedDict, total=False): + dest_path: Required[str] + """Absolute destination directory to extract the archive to.""" + + zip_file: Required[FileTypes] diff --git a/src/kernel/types/browsers/log_stream_params.py b/src/kernel/types/browsers/log_stream_params.py new file mode 100644 index 00000000..2eeb9b32 --- /dev/null +++ b/src/kernel/types/browsers/log_stream_params.py @@ -0,0 +1,19 @@ +# 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__ = ["LogStreamParams"] + + +class LogStreamParams(TypedDict, total=False): + source: Required[Literal["path", "supervisor"]] + + follow: bool + + path: str + """only required if source is path""" + + supervisor_process: str + """only required if source is supervisor""" diff --git a/src/kernel/types/browsers/process_exec_params.py b/src/kernel/types/browsers/process_exec_params.py new file mode 100644 index 00000000..3dd3ad59 --- /dev/null +++ b/src/kernel/types/browsers/process_exec_params.py @@ -0,0 +1,31 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, List, Optional +from typing_extensions import Required, TypedDict + +__all__ = ["ProcessExecParams"] + + +class ProcessExecParams(TypedDict, total=False): + command: Required[str] + """Executable or shell command to run.""" + + args: List[str] + """Command arguments.""" + + as_root: bool + """Run the process with root privileges.""" + + as_user: Optional[str] + """Run the process as this user.""" + + cwd: Optional[str] + """Working directory (absolute path) to run the command in.""" + + env: Dict[str, str] + """Environment variables to set for the process.""" + + timeout_sec: Optional[int] + """Maximum execution time in seconds.""" diff --git a/src/kernel/types/browsers/process_exec_response.py b/src/kernel/types/browsers/process_exec_response.py new file mode 100644 index 00000000..02588de0 --- /dev/null +++ b/src/kernel/types/browsers/process_exec_response.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = ["ProcessExecResponse"] + + +class ProcessExecResponse(BaseModel): + duration_ms: Optional[int] = None + """Execution duration in milliseconds.""" + + exit_code: Optional[int] = None + """Process exit code.""" + + stderr_b64: Optional[str] = None + """Base64-encoded stderr buffer.""" + + stdout_b64: Optional[str] = None + """Base64-encoded stdout buffer.""" diff --git a/src/kernel/types/browsers/process_kill_params.py b/src/kernel/types/browsers/process_kill_params.py new file mode 100644 index 00000000..84be2770 --- /dev/null +++ b/src/kernel/types/browsers/process_kill_params.py @@ -0,0 +1,14 @@ +# 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__ = ["ProcessKillParams"] + + +class ProcessKillParams(TypedDict, total=False): + id: Required[str] + + signal: Required[Literal["TERM", "KILL", "INT", "HUP"]] + """Signal to send.""" diff --git a/src/kernel/types/browsers/process_kill_response.py b/src/kernel/types/browsers/process_kill_response.py new file mode 100644 index 00000000..ed128a78 --- /dev/null +++ b/src/kernel/types/browsers/process_kill_response.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from ..._models import BaseModel + +__all__ = ["ProcessKillResponse"] + + +class ProcessKillResponse(BaseModel): + ok: bool + """Indicates success.""" diff --git a/src/kernel/types/browsers/process_spawn_params.py b/src/kernel/types/browsers/process_spawn_params.py new file mode 100644 index 00000000..a468c473 --- /dev/null +++ b/src/kernel/types/browsers/process_spawn_params.py @@ -0,0 +1,31 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, List, Optional +from typing_extensions import Required, TypedDict + +__all__ = ["ProcessSpawnParams"] + + +class ProcessSpawnParams(TypedDict, total=False): + command: Required[str] + """Executable or shell command to run.""" + + args: List[str] + """Command arguments.""" + + as_root: bool + """Run the process with root privileges.""" + + as_user: Optional[str] + """Run the process as this user.""" + + cwd: Optional[str] + """Working directory (absolute path) to run the command in.""" + + env: Dict[str, str] + """Environment variables to set for the process.""" + + timeout_sec: Optional[int] + """Maximum execution time in seconds.""" diff --git a/src/kernel/types/browsers/process_spawn_response.py b/src/kernel/types/browsers/process_spawn_response.py new file mode 100644 index 00000000..23444da2 --- /dev/null +++ b/src/kernel/types/browsers/process_spawn_response.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime + +from ..._models import BaseModel + +__all__ = ["ProcessSpawnResponse"] + + +class ProcessSpawnResponse(BaseModel): + pid: Optional[int] = None + """OS process ID.""" + + process_id: Optional[str] = None + """Server-assigned identifier for the process.""" + + started_at: Optional[datetime] = None + """Timestamp when the process started.""" diff --git a/src/kernel/types/browsers/process_status_response.py b/src/kernel/types/browsers/process_status_response.py new file mode 100644 index 00000000..67626fe9 --- /dev/null +++ b/src/kernel/types/browsers/process_status_response.py @@ -0,0 +1,22 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["ProcessStatusResponse"] + + +class ProcessStatusResponse(BaseModel): + cpu_pct: Optional[float] = None + """Estimated CPU usage percentage.""" + + exit_code: Optional[int] = None + """Exit code if the process has exited.""" + + mem_bytes: Optional[int] = None + """Estimated resident memory usage in bytes.""" + + state: Optional[Literal["running", "exited"]] = None + """Process state.""" diff --git a/src/kernel/types/browsers/process_stdin_params.py b/src/kernel/types/browsers/process_stdin_params.py new file mode 100644 index 00000000..9ece9a57 --- /dev/null +++ b/src/kernel/types/browsers/process_stdin_params.py @@ -0,0 +1,14 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["ProcessStdinParams"] + + +class ProcessStdinParams(TypedDict, total=False): + id: Required[str] + + data_b64: Required[str] + """Base64-encoded data to write.""" diff --git a/src/kernel/types/browsers/process_stdin_response.py b/src/kernel/types/browsers/process_stdin_response.py new file mode 100644 index 00000000..d137a962 --- /dev/null +++ b/src/kernel/types/browsers/process_stdin_response.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = ["ProcessStdinResponse"] + + +class ProcessStdinResponse(BaseModel): + written_bytes: Optional[int] = None + """Number of bytes written.""" diff --git a/src/kernel/types/browsers/process_stdout_stream_response.py b/src/kernel/types/browsers/process_stdout_stream_response.py new file mode 100644 index 00000000..0b1d0a8e --- /dev/null +++ b/src/kernel/types/browsers/process_stdout_stream_response.py @@ -0,0 +1,22 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["ProcessStdoutStreamResponse"] + + +class ProcessStdoutStreamResponse(BaseModel): + data_b64: Optional[str] = None + """Base64-encoded data from the process stream.""" + + event: Optional[Literal["exit"]] = None + """Lifecycle event type.""" + + exit_code: Optional[int] = None + """Exit code when the event is "exit".""" + + stream: Optional[Literal["stdout", "stderr"]] = None + """Source stream of the data chunk.""" diff --git a/tests/api_resources/browsers/test_fs.py b/tests/api_resources/browsers/test_fs.py index 24860c90..38e07b78 100644 --- a/tests/api_resources/browsers/test_fs.py +++ b/tests/api_resources/browsers/test_fs.py @@ -176,6 +176,60 @@ def test_path_params_delete_file(self, client: Kernel) -> None: path="/J!", ) + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_method_download_dir_zip(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/id/fs/download_dir_zip").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + f = client.browsers.fs.download_dir_zip( + id="id", + path="/J!", + ) + assert f.is_closed + assert f.json() == {"foo": "bar"} + assert cast(Any, f.is_closed) is True + assert isinstance(f, BinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_raw_response_download_dir_zip(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/id/fs/download_dir_zip").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + f = client.browsers.fs.with_raw_response.download_dir_zip( + id="id", + path="/J!", + ) + + assert f.is_closed is True + assert f.http_request.headers.get("X-Stainless-Lang") == "python" + assert f.json() == {"foo": "bar"} + assert isinstance(f, BinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_streaming_response_download_dir_zip(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/id/fs/download_dir_zip").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + with client.browsers.fs.with_streaming_response.download_dir_zip( + id="id", + path="/J!", + ) as f: + assert not f.is_closed + assert f.http_request.headers.get("X-Stainless-Lang") == "python" + + assert f.json() == {"foo": "bar"} + assert cast(Any, f.is_closed) is True + assert isinstance(f, StreamedBinaryAPIResponse) + + assert cast(Any, f.is_closed) is True + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_path_params_download_dir_zip(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.fs.with_raw_response.download_dir_zip( + id="", + path="/J!", + ) + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_file_info(self, client: Kernel) -> None: @@ -434,6 +488,122 @@ def test_path_params_set_file_permissions(self, client: Kernel) -> None: path="/J!", ) + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_upload(self, client: Kernel) -> None: + f = client.browsers.fs.upload( + id="id", + files=[ + { + "dest_path": "/J!", + "file": b"raw file contents", + } + ], + ) + assert f is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_upload(self, client: Kernel) -> None: + response = client.browsers.fs.with_raw_response.upload( + id="id", + files=[ + { + "dest_path": "/J!", + "file": b"raw file contents", + } + ], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + f = response.parse() + assert f is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_upload(self, client: Kernel) -> None: + with client.browsers.fs.with_streaming_response.upload( + id="id", + files=[ + { + "dest_path": "/J!", + "file": b"raw file contents", + } + ], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + f = response.parse() + assert f is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_upload(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.fs.with_raw_response.upload( + id="", + files=[ + { + "dest_path": "/J!", + "file": b"raw file contents", + } + ], + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_upload_zip(self, client: Kernel) -> None: + f = client.browsers.fs.upload_zip( + id="id", + dest_path="/J!", + zip_file=b"raw file contents", + ) + assert f is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_upload_zip(self, client: Kernel) -> None: + response = client.browsers.fs.with_raw_response.upload_zip( + id="id", + dest_path="/J!", + zip_file=b"raw file contents", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + f = response.parse() + assert f is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_upload_zip(self, client: Kernel) -> None: + with client.browsers.fs.with_streaming_response.upload_zip( + id="id", + dest_path="/J!", + zip_file=b"raw file contents", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + f = response.parse() + assert f is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_upload_zip(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.fs.with_raw_response.upload_zip( + id="", + dest_path="/J!", + zip_file=b"raw file contents", + ) + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_write_file(self, client: Kernel) -> None: @@ -649,6 +819,60 @@ async def test_path_params_delete_file(self, async_client: AsyncKernel) -> None: path="/J!", ) + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_method_download_dir_zip(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/id/fs/download_dir_zip").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + f = await async_client.browsers.fs.download_dir_zip( + id="id", + path="/J!", + ) + assert f.is_closed + assert await f.json() == {"foo": "bar"} + assert cast(Any, f.is_closed) is True + assert isinstance(f, AsyncBinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_raw_response_download_dir_zip(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/id/fs/download_dir_zip").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + f = await async_client.browsers.fs.with_raw_response.download_dir_zip( + id="id", + path="/J!", + ) + + assert f.is_closed is True + assert f.http_request.headers.get("X-Stainless-Lang") == "python" + assert await f.json() == {"foo": "bar"} + assert isinstance(f, AsyncBinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_streaming_response_download_dir_zip(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/id/fs/download_dir_zip").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + async with async_client.browsers.fs.with_streaming_response.download_dir_zip( + id="id", + path="/J!", + ) as f: + assert not f.is_closed + assert f.http_request.headers.get("X-Stainless-Lang") == "python" + + assert await f.json() == {"foo": "bar"} + assert cast(Any, f.is_closed) is True + assert isinstance(f, AsyncStreamedBinaryAPIResponse) + + assert cast(Any, f.is_closed) is True + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_path_params_download_dir_zip(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.fs.with_raw_response.download_dir_zip( + id="", + path="/J!", + ) + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_file_info(self, async_client: AsyncKernel) -> None: @@ -907,6 +1131,122 @@ async def test_path_params_set_file_permissions(self, async_client: AsyncKernel) path="/J!", ) + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_upload(self, async_client: AsyncKernel) -> None: + f = await async_client.browsers.fs.upload( + id="id", + files=[ + { + "dest_path": "/J!", + "file": b"raw file contents", + } + ], + ) + assert f is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_upload(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.fs.with_raw_response.upload( + id="id", + files=[ + { + "dest_path": "/J!", + "file": b"raw file contents", + } + ], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + f = await response.parse() + assert f is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_upload(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.fs.with_streaming_response.upload( + id="id", + files=[ + { + "dest_path": "/J!", + "file": b"raw file contents", + } + ], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + f = await response.parse() + assert f is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_upload(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.fs.with_raw_response.upload( + id="", + files=[ + { + "dest_path": "/J!", + "file": b"raw file contents", + } + ], + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_upload_zip(self, async_client: AsyncKernel) -> None: + f = await async_client.browsers.fs.upload_zip( + id="id", + dest_path="/J!", + zip_file=b"raw file contents", + ) + assert f is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_upload_zip(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.fs.with_raw_response.upload_zip( + id="id", + dest_path="/J!", + zip_file=b"raw file contents", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + f = await response.parse() + assert f is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_upload_zip(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.fs.with_streaming_response.upload_zip( + id="id", + dest_path="/J!", + zip_file=b"raw file contents", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + f = await response.parse() + assert f is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_upload_zip(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.fs.with_raw_response.upload_zip( + id="", + dest_path="/J!", + zip_file=b"raw file contents", + ) + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_write_file(self, async_client: AsyncKernel) -> None: diff --git a/tests/api_resources/browsers/test_logs.py b/tests/api_resources/browsers/test_logs.py new file mode 100644 index 00000000..6aac62f6 --- /dev/null +++ b/tests/api_resources/browsers/test_logs.py @@ -0,0 +1,136 @@ +# 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 kernel import Kernel, AsyncKernel + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestLogs: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + def test_method_stream(self, client: Kernel) -> None: + log_stream = client.browsers.logs.stream( + id="id", + source="path", + ) + log_stream.response.close() + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + def test_method_stream_with_all_params(self, client: Kernel) -> None: + log_stream = client.browsers.logs.stream( + id="id", + source="path", + follow=True, + path="path", + supervisor_process="supervisor_process", + ) + log_stream.response.close() + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + def test_raw_response_stream(self, client: Kernel) -> None: + response = client.browsers.logs.with_raw_response.stream( + id="id", + source="path", + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = response.parse() + stream.close() + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + def test_streaming_response_stream(self, client: Kernel) -> None: + with client.browsers.logs.with_streaming_response.stream( + id="id", + source="path", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = response.parse() + stream.close() + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + def test_path_params_stream(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.logs.with_raw_response.stream( + id="", + source="path", + ) + + +class TestAsyncLogs: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + async def test_method_stream(self, async_client: AsyncKernel) -> None: + log_stream = await async_client.browsers.logs.stream( + id="id", + source="path", + ) + await log_stream.response.aclose() + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + async def test_method_stream_with_all_params(self, async_client: AsyncKernel) -> None: + log_stream = await async_client.browsers.logs.stream( + id="id", + source="path", + follow=True, + path="path", + supervisor_process="supervisor_process", + ) + await log_stream.response.aclose() + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + async def test_raw_response_stream(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.logs.with_raw_response.stream( + id="id", + source="path", + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = await response.parse() + await stream.close() + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + async def test_streaming_response_stream(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.logs.with_streaming_response.stream( + id="id", + source="path", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = await response.parse() + await stream.close() + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + async def test_path_params_stream(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.logs.with_raw_response.stream( + id="", + source="path", + ) diff --git a/tests/api_resources/browsers/test_process.py b/tests/api_resources/browsers/test_process.py new file mode 100644 index 00000000..69977621 --- /dev/null +++ b/tests/api_resources/browsers/test_process.py @@ -0,0 +1,708 @@ +# 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 kernel import Kernel, AsyncKernel +from tests.utils import assert_matches_type +from kernel.types.browsers import ( + ProcessExecResponse, + ProcessKillResponse, + ProcessSpawnResponse, + ProcessStdinResponse, + ProcessStatusResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestProcess: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_exec(self, client: Kernel) -> None: + process = client.browsers.process.exec( + id="id", + command="command", + ) + assert_matches_type(ProcessExecResponse, process, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_exec_with_all_params(self, client: Kernel) -> None: + process = client.browsers.process.exec( + id="id", + command="command", + args=["string"], + as_root=True, + as_user="as_user", + cwd="/J!", + env={"foo": "string"}, + timeout_sec=0, + ) + assert_matches_type(ProcessExecResponse, process, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_exec(self, client: Kernel) -> None: + response = client.browsers.process.with_raw_response.exec( + id="id", + command="command", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + process = response.parse() + assert_matches_type(ProcessExecResponse, process, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_exec(self, client: Kernel) -> None: + with client.browsers.process.with_streaming_response.exec( + id="id", + command="command", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + process = response.parse() + assert_matches_type(ProcessExecResponse, process, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_exec(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.process.with_raw_response.exec( + id="", + command="command", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_kill(self, client: Kernel) -> None: + process = client.browsers.process.kill( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + signal="TERM", + ) + assert_matches_type(ProcessKillResponse, process, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_kill(self, client: Kernel) -> None: + response = client.browsers.process.with_raw_response.kill( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + signal="TERM", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + process = response.parse() + assert_matches_type(ProcessKillResponse, process, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_kill(self, client: Kernel) -> None: + with client.browsers.process.with_streaming_response.kill( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + signal="TERM", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + process = response.parse() + assert_matches_type(ProcessKillResponse, process, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_kill(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.process.with_raw_response.kill( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="", + signal="TERM", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `process_id` but received ''"): + client.browsers.process.with_raw_response.kill( + process_id="", + id="id", + signal="TERM", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_spawn(self, client: Kernel) -> None: + process = client.browsers.process.spawn( + id="id", + command="command", + ) + assert_matches_type(ProcessSpawnResponse, process, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_spawn_with_all_params(self, client: Kernel) -> None: + process = client.browsers.process.spawn( + id="id", + command="command", + args=["string"], + as_root=True, + as_user="as_user", + cwd="/J!", + env={"foo": "string"}, + timeout_sec=0, + ) + assert_matches_type(ProcessSpawnResponse, process, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_spawn(self, client: Kernel) -> None: + response = client.browsers.process.with_raw_response.spawn( + id="id", + command="command", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + process = response.parse() + assert_matches_type(ProcessSpawnResponse, process, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_spawn(self, client: Kernel) -> None: + with client.browsers.process.with_streaming_response.spawn( + id="id", + command="command", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + process = response.parse() + assert_matches_type(ProcessSpawnResponse, process, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_spawn(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.process.with_raw_response.spawn( + id="", + command="command", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_status(self, client: Kernel) -> None: + process = client.browsers.process.status( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + ) + assert_matches_type(ProcessStatusResponse, process, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_status(self, client: Kernel) -> None: + response = client.browsers.process.with_raw_response.status( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + process = response.parse() + assert_matches_type(ProcessStatusResponse, process, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_status(self, client: Kernel) -> None: + with client.browsers.process.with_streaming_response.status( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + process = response.parse() + assert_matches_type(ProcessStatusResponse, process, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_status(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.process.with_raw_response.status( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `process_id` but received ''"): + client.browsers.process.with_raw_response.status( + process_id="", + id="id", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_stdin(self, client: Kernel) -> None: + process = client.browsers.process.stdin( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + data_b64="data_b64", + ) + assert_matches_type(ProcessStdinResponse, process, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_stdin(self, client: Kernel) -> None: + response = client.browsers.process.with_raw_response.stdin( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + data_b64="data_b64", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + process = response.parse() + assert_matches_type(ProcessStdinResponse, process, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_stdin(self, client: Kernel) -> None: + with client.browsers.process.with_streaming_response.stdin( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + data_b64="data_b64", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + process = response.parse() + assert_matches_type(ProcessStdinResponse, process, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_stdin(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.process.with_raw_response.stdin( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="", + data_b64="data_b64", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `process_id` but received ''"): + client.browsers.process.with_raw_response.stdin( + process_id="", + id="id", + data_b64="data_b64", + ) + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + def test_method_stdout_stream(self, client: Kernel) -> None: + process_stream = client.browsers.process.stdout_stream( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + ) + process_stream.response.close() + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + def test_raw_response_stdout_stream(self, client: Kernel) -> None: + response = client.browsers.process.with_raw_response.stdout_stream( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = response.parse() + stream.close() + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + def test_streaming_response_stdout_stream(self, client: Kernel) -> None: + with client.browsers.process.with_streaming_response.stdout_stream( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = response.parse() + stream.close() + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + def test_path_params_stdout_stream(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.process.with_raw_response.stdout_stream( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `process_id` but received ''"): + client.browsers.process.with_raw_response.stdout_stream( + process_id="", + id="id", + ) + + +class TestAsyncProcess: + 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_exec(self, async_client: AsyncKernel) -> None: + process = await async_client.browsers.process.exec( + id="id", + command="command", + ) + assert_matches_type(ProcessExecResponse, process, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_exec_with_all_params(self, async_client: AsyncKernel) -> None: + process = await async_client.browsers.process.exec( + id="id", + command="command", + args=["string"], + as_root=True, + as_user="as_user", + cwd="/J!", + env={"foo": "string"}, + timeout_sec=0, + ) + assert_matches_type(ProcessExecResponse, process, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_exec(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.process.with_raw_response.exec( + id="id", + command="command", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + process = await response.parse() + assert_matches_type(ProcessExecResponse, process, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_exec(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.process.with_streaming_response.exec( + id="id", + command="command", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + process = await response.parse() + assert_matches_type(ProcessExecResponse, process, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_exec(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.process.with_raw_response.exec( + id="", + command="command", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_kill(self, async_client: AsyncKernel) -> None: + process = await async_client.browsers.process.kill( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + signal="TERM", + ) + assert_matches_type(ProcessKillResponse, process, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_kill(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.process.with_raw_response.kill( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + signal="TERM", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + process = await response.parse() + assert_matches_type(ProcessKillResponse, process, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_kill(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.process.with_streaming_response.kill( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + signal="TERM", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + process = await response.parse() + assert_matches_type(ProcessKillResponse, process, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_kill(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.process.with_raw_response.kill( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="", + signal="TERM", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `process_id` but received ''"): + await async_client.browsers.process.with_raw_response.kill( + process_id="", + id="id", + signal="TERM", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_spawn(self, async_client: AsyncKernel) -> None: + process = await async_client.browsers.process.spawn( + id="id", + command="command", + ) + assert_matches_type(ProcessSpawnResponse, process, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_spawn_with_all_params(self, async_client: AsyncKernel) -> None: + process = await async_client.browsers.process.spawn( + id="id", + command="command", + args=["string"], + as_root=True, + as_user="as_user", + cwd="/J!", + env={"foo": "string"}, + timeout_sec=0, + ) + assert_matches_type(ProcessSpawnResponse, process, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_spawn(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.process.with_raw_response.spawn( + id="id", + command="command", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + process = await response.parse() + assert_matches_type(ProcessSpawnResponse, process, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_spawn(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.process.with_streaming_response.spawn( + id="id", + command="command", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + process = await response.parse() + assert_matches_type(ProcessSpawnResponse, process, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_spawn(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.process.with_raw_response.spawn( + id="", + command="command", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_status(self, async_client: AsyncKernel) -> None: + process = await async_client.browsers.process.status( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + ) + assert_matches_type(ProcessStatusResponse, process, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_status(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.process.with_raw_response.status( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + process = await response.parse() + assert_matches_type(ProcessStatusResponse, process, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_status(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.process.with_streaming_response.status( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + process = await response.parse() + assert_matches_type(ProcessStatusResponse, process, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_status(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.process.with_raw_response.status( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `process_id` but received ''"): + await async_client.browsers.process.with_raw_response.status( + process_id="", + id="id", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_stdin(self, async_client: AsyncKernel) -> None: + process = await async_client.browsers.process.stdin( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + data_b64="data_b64", + ) + assert_matches_type(ProcessStdinResponse, process, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_stdin(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.process.with_raw_response.stdin( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + data_b64="data_b64", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + process = await response.parse() + assert_matches_type(ProcessStdinResponse, process, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_stdin(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.process.with_streaming_response.stdin( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + data_b64="data_b64", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + process = await response.parse() + assert_matches_type(ProcessStdinResponse, process, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_stdin(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.process.with_raw_response.stdin( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="", + data_b64="data_b64", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `process_id` but received ''"): + await async_client.browsers.process.with_raw_response.stdin( + process_id="", + id="id", + data_b64="data_b64", + ) + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + async def test_method_stdout_stream(self, async_client: AsyncKernel) -> None: + process_stream = await async_client.browsers.process.stdout_stream( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + ) + await process_stream.response.aclose() + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + async def test_raw_response_stdout_stream(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.process.with_raw_response.stdout_stream( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = await response.parse() + await stream.close() + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + async def test_streaming_response_stdout_stream(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.process.with_streaming_response.stdout_stream( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = await response.parse() + await stream.close() + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + async def test_path_params_stdout_stream(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.process.with_raw_response.stdout_stream( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `process_id` but received ''"): + await async_client.browsers.process.with_raw_response.stdout_stream( + process_id="", + id="id", + ) From c617368c2ca39a2f40c4c6464f72912358c07775 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 27 Aug 2025 18:21:55 +0000 Subject: [PATCH 151/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 05988747..091cfb12 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.9.1" + ".": "0.10.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 584c3677..3cc0fb34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.9.1" +version = "0.10.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index de748b73..db4afd4b 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.9.1" # x-release-please-version +__version__ = "0.10.0" # x-release-please-version From b891f39a33883260f8b18e2fef7fdb3ad9751ddf Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 30 Aug 2025 03:46:18 +0000 Subject: [PATCH 152/448] chore(internal): add Sequence related utils --- src/kernel/_types.py | 36 ++++++++++++++++++++++++++++++++++- src/kernel/_utils/__init__.py | 1 + src/kernel/_utils/_typing.py | 5 +++++ tests/utils.py | 10 +++++++++- 4 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/kernel/_types.py b/src/kernel/_types.py index 18a1ef53..48bae95e 100644 --- a/src/kernel/_types.py +++ b/src/kernel/_types.py @@ -13,10 +13,21 @@ Mapping, TypeVar, Callable, + Iterator, Optional, Sequence, ) -from typing_extensions import Set, Literal, Protocol, TypeAlias, TypedDict, override, runtime_checkable +from typing_extensions import ( + Set, + Literal, + Protocol, + TypeAlias, + TypedDict, + SupportsIndex, + overload, + override, + runtime_checkable, +) import httpx import pydantic @@ -217,3 +228,26 @@ class _GenericAlias(Protocol): class HttpxSendArgs(TypedDict, total=False): auth: httpx.Auth follow_redirects: bool + + +_T_co = TypeVar("_T_co", covariant=True) + + +if TYPE_CHECKING: + # This works because str.__contains__ does not accept object (either in typeshed or at runtime) + # https://github.com/hauntsaninja/useful_types/blob/5e9710f3875107d068e7679fd7fec9cfab0eff3b/useful_types/__init__.py#L285 + class SequenceNotStr(Protocol[_T_co]): + @overload + def __getitem__(self, index: SupportsIndex, /) -> _T_co: ... + @overload + def __getitem__(self, index: slice, /) -> Sequence[_T_co]: ... + def __contains__(self, value: object, /) -> bool: ... + def __len__(self) -> int: ... + def __iter__(self) -> Iterator[_T_co]: ... + def index(self, value: Any, start: int = 0, stop: int = ..., /) -> int: ... + def count(self, value: Any, /) -> int: ... + def __reversed__(self) -> Iterator[_T_co]: ... +else: + # just point this to a normal `Sequence` at runtime to avoid having to special case + # deserializing our custom sequence type + SequenceNotStr = Sequence diff --git a/src/kernel/_utils/__init__.py b/src/kernel/_utils/__init__.py index d4fda26f..ca547ce5 100644 --- a/src/kernel/_utils/__init__.py +++ b/src/kernel/_utils/__init__.py @@ -38,6 +38,7 @@ extract_type_arg as extract_type_arg, is_iterable_type as is_iterable_type, is_required_type as is_required_type, + is_sequence_type as is_sequence_type, is_annotated_type as is_annotated_type, is_type_alias_type as is_type_alias_type, strip_annotated_type as strip_annotated_type, diff --git a/src/kernel/_utils/_typing.py b/src/kernel/_utils/_typing.py index 1bac9542..845cd6b2 100644 --- a/src/kernel/_utils/_typing.py +++ b/src/kernel/_utils/_typing.py @@ -26,6 +26,11 @@ def is_list_type(typ: type) -> bool: return (get_origin(typ) or typ) == list +def is_sequence_type(typ: type) -> bool: + origin = get_origin(typ) or typ + return origin == typing_extensions.Sequence or origin == typing.Sequence or origin == _c_abc.Sequence + + def is_iterable_type(typ: type) -> bool: """If the given type is `typing.Iterable[T]`""" origin = get_origin(typ) or typ diff --git a/tests/utils.py b/tests/utils.py index d81c8f4b..3e90233d 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -4,7 +4,7 @@ import inspect import traceback import contextlib -from typing import Any, TypeVar, Iterator, cast +from typing import Any, TypeVar, Iterator, Sequence, cast from datetime import date, datetime from typing_extensions import Literal, get_args, get_origin, assert_type @@ -15,6 +15,7 @@ is_list_type, is_union_type, extract_type_arg, + is_sequence_type, is_annotated_type, is_type_alias_type, ) @@ -71,6 +72,13 @@ def assert_matches_type( if is_list_type(type_): return _assert_list_type(type_, value) + if is_sequence_type(type_): + assert isinstance(value, Sequence) + inner_type = get_args(type_)[0] + for entry in value: # type: ignore + assert_type(inner_type, entry) # type: ignore + return + if origin == str: assert isinstance(value, str) elif origin == int: From 032dee9b89a380b9b39e1d958ba8626bcf62e647 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 3 Sep 2025 03:10:14 +0000 Subject: [PATCH 153/448] feat(types): replace List[str] with SequenceNotStr in params --- src/kernel/_utils/_transform.py | 6 ++++++ src/kernel/resources/browsers/process.py | 12 ++++++------ src/kernel/types/browsers/process_exec_params.py | 6 ++++-- src/kernel/types/browsers/process_spawn_params.py | 6 ++++-- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/kernel/_utils/_transform.py b/src/kernel/_utils/_transform.py index b0cc20a7..f0bcefd4 100644 --- a/src/kernel/_utils/_transform.py +++ b/src/kernel/_utils/_transform.py @@ -16,6 +16,7 @@ lru_cache, is_mapping, is_iterable, + is_sequence, ) from .._files import is_base64_file_input from ._typing import ( @@ -24,6 +25,7 @@ extract_type_arg, is_iterable_type, is_required_type, + is_sequence_type, is_annotated_type, strip_annotated_type, ) @@ -184,6 +186,8 @@ def _transform_recursive( (is_list_type(stripped_type) and is_list(data)) # Iterable[T] or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str)) + # Sequence[T] + or (is_sequence_type(stripped_type) and is_sequence(data) and not isinstance(data, str)) ): # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually # intended as an iterable, so we don't transform it. @@ -346,6 +350,8 @@ async def _async_transform_recursive( (is_list_type(stripped_type) and is_list(data)) # Iterable[T] or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str)) + # Sequence[T] + or (is_sequence_type(stripped_type) and is_sequence(data) and not isinstance(data, str)) ): # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually # intended as an iterable, so we don't transform it. diff --git a/src/kernel/resources/browsers/process.py b/src/kernel/resources/browsers/process.py index d3f5eca5..9fd6dd6c 100644 --- a/src/kernel/resources/browsers/process.py +++ b/src/kernel/resources/browsers/process.py @@ -2,12 +2,12 @@ from __future__ import annotations -from typing import Dict, List, Optional +from typing import Dict, Optional from typing_extensions import Literal import httpx -from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, SequenceNotStr from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource @@ -55,7 +55,7 @@ def exec( id: str, *, command: str, - args: List[str] | NotGiven = NOT_GIVEN, + args: SequenceNotStr[str] | NotGiven = NOT_GIVEN, as_root: bool | NotGiven = NOT_GIVEN, as_user: Optional[str] | NotGiven = NOT_GIVEN, cwd: Optional[str] | NotGiven = NOT_GIVEN, @@ -161,7 +161,7 @@ def spawn( id: str, *, command: str, - args: List[str] | NotGiven = NOT_GIVEN, + args: SequenceNotStr[str] | NotGiven = NOT_GIVEN, as_root: bool | NotGiven = NOT_GIVEN, as_user: Optional[str] | NotGiven = NOT_GIVEN, cwd: Optional[str] | NotGiven = NOT_GIVEN, @@ -363,7 +363,7 @@ async def exec( id: str, *, command: str, - args: List[str] | NotGiven = NOT_GIVEN, + args: SequenceNotStr[str] | NotGiven = NOT_GIVEN, as_root: bool | NotGiven = NOT_GIVEN, as_user: Optional[str] | NotGiven = NOT_GIVEN, cwd: Optional[str] | NotGiven = NOT_GIVEN, @@ -469,7 +469,7 @@ async def spawn( id: str, *, command: str, - args: List[str] | NotGiven = NOT_GIVEN, + args: SequenceNotStr[str] | NotGiven = NOT_GIVEN, as_root: bool | NotGiven = NOT_GIVEN, as_user: Optional[str] | NotGiven = NOT_GIVEN, cwd: Optional[str] | NotGiven = NOT_GIVEN, diff --git a/src/kernel/types/browsers/process_exec_params.py b/src/kernel/types/browsers/process_exec_params.py index 3dd3ad59..a6481c19 100644 --- a/src/kernel/types/browsers/process_exec_params.py +++ b/src/kernel/types/browsers/process_exec_params.py @@ -2,9 +2,11 @@ from __future__ import annotations -from typing import Dict, List, Optional +from typing import Dict, Optional from typing_extensions import Required, TypedDict +from ..._types import SequenceNotStr + __all__ = ["ProcessExecParams"] @@ -12,7 +14,7 @@ class ProcessExecParams(TypedDict, total=False): command: Required[str] """Executable or shell command to run.""" - args: List[str] + args: SequenceNotStr[str] """Command arguments.""" as_root: bool diff --git a/src/kernel/types/browsers/process_spawn_params.py b/src/kernel/types/browsers/process_spawn_params.py index a468c473..8e901cb0 100644 --- a/src/kernel/types/browsers/process_spawn_params.py +++ b/src/kernel/types/browsers/process_spawn_params.py @@ -2,9 +2,11 @@ from __future__ import annotations -from typing import Dict, List, Optional +from typing import Dict, Optional from typing_extensions import Required, TypedDict +from ..._types import SequenceNotStr + __all__ = ["ProcessSpawnParams"] @@ -12,7 +14,7 @@ class ProcessSpawnParams(TypedDict, total=False): command: Required[str] """Executable or shell command to run.""" - args: List[str] + args: SequenceNotStr[str] """Command arguments.""" as_root: bool From 387a173738b67ac2d576e0b8d6da93012eb4b5b2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 03:11:55 +0000 Subject: [PATCH 154/448] feat: improve future compat with pydantic v3 --- src/kernel/_base_client.py | 6 +- src/kernel/_compat.py | 96 ++++++++--------- src/kernel/_models.py | 80 +++++++------- src/kernel/_utils/__init__.py | 10 +- src/kernel/_utils/_compat.py | 45 ++++++++ src/kernel/_utils/_datetime_parse.py | 136 ++++++++++++++++++++++++ src/kernel/_utils/_transform.py | 6 +- src/kernel/_utils/_typing.py | 2 +- src/kernel/_utils/_utils.py | 1 - tests/test_models.py | 48 ++++----- tests/test_transform.py | 16 +-- tests/test_utils/test_datetime_parse.py | 110 +++++++++++++++++++ tests/utils.py | 8 +- 13 files changed, 432 insertions(+), 132 deletions(-) create mode 100644 src/kernel/_utils/_compat.py create mode 100644 src/kernel/_utils/_datetime_parse.py create mode 100644 tests/test_utils/test_datetime_parse.py diff --git a/src/kernel/_base_client.py b/src/kernel/_base_client.py index 79cd0901..0d2ff453 100644 --- a/src/kernel/_base_client.py +++ b/src/kernel/_base_client.py @@ -59,7 +59,7 @@ ModelBuilderProtocol, ) from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping -from ._compat import PYDANTIC_V2, model_copy, model_dump +from ._compat import PYDANTIC_V1, model_copy, model_dump from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type from ._response import ( APIResponse, @@ -232,7 +232,7 @@ def _set_private_attributes( model: Type[_T], options: FinalRequestOptions, ) -> None: - if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None: + if (not PYDANTIC_V1) and getattr(self, "__pydantic_private__", None) is None: self.__pydantic_private__ = {} self._model = model @@ -320,7 +320,7 @@ def _set_private_attributes( client: AsyncAPIClient, options: FinalRequestOptions, ) -> None: - if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None: + if (not PYDANTIC_V1) and getattr(self, "__pydantic_private__", None) is None: self.__pydantic_private__ = {} self._model = model diff --git a/src/kernel/_compat.py b/src/kernel/_compat.py index 92d9ee61..bdef67f0 100644 --- a/src/kernel/_compat.py +++ b/src/kernel/_compat.py @@ -12,14 +12,13 @@ _T = TypeVar("_T") _ModelT = TypeVar("_ModelT", bound=pydantic.BaseModel) -# --------------- Pydantic v2 compatibility --------------- +# --------------- Pydantic v2, v3 compatibility --------------- # Pyright incorrectly reports some of our functions as overriding a method when they don't # pyright: reportIncompatibleMethodOverride=false -PYDANTIC_V2 = pydantic.VERSION.startswith("2.") +PYDANTIC_V1 = pydantic.VERSION.startswith("1.") -# v1 re-exports if TYPE_CHECKING: def parse_date(value: date | StrBytesIntFloat) -> date: # noqa: ARG001 @@ -44,90 +43,92 @@ def is_typeddict(type_: type[Any]) -> bool: # noqa: ARG001 ... else: - if PYDANTIC_V2: - from pydantic.v1.typing import ( + # v1 re-exports + if PYDANTIC_V1: + from pydantic.typing import ( get_args as get_args, is_union as is_union, get_origin as get_origin, is_typeddict as is_typeddict, is_literal_type as is_literal_type, ) - from pydantic.v1.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime + from pydantic.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime else: - from pydantic.typing import ( + from ._utils import ( get_args as get_args, is_union as is_union, get_origin as get_origin, + parse_date as parse_date, is_typeddict as is_typeddict, + parse_datetime as parse_datetime, is_literal_type as is_literal_type, ) - from pydantic.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime # refactored config if TYPE_CHECKING: from pydantic import ConfigDict as ConfigDict else: - if PYDANTIC_V2: - from pydantic import ConfigDict - else: + if PYDANTIC_V1: # TODO: provide an error message here? ConfigDict = None + else: + from pydantic import ConfigDict as ConfigDict # renamed methods / properties def parse_obj(model: type[_ModelT], value: object) -> _ModelT: - if PYDANTIC_V2: - return model.model_validate(value) - else: + if PYDANTIC_V1: return cast(_ModelT, model.parse_obj(value)) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] + else: + return model.model_validate(value) def field_is_required(field: FieldInfo) -> bool: - if PYDANTIC_V2: - return field.is_required() - return field.required # type: ignore + if PYDANTIC_V1: + return field.required # type: ignore + return field.is_required() def field_get_default(field: FieldInfo) -> Any: value = field.get_default() - if PYDANTIC_V2: - from pydantic_core import PydanticUndefined - - if value == PydanticUndefined: - return None + if PYDANTIC_V1: return value + from pydantic_core import PydanticUndefined + + if value == PydanticUndefined: + return None return value def field_outer_type(field: FieldInfo) -> Any: - if PYDANTIC_V2: - return field.annotation - return field.outer_type_ # type: ignore + if PYDANTIC_V1: + return field.outer_type_ # type: ignore + return field.annotation def get_model_config(model: type[pydantic.BaseModel]) -> Any: - if PYDANTIC_V2: - return model.model_config - return model.__config__ # type: ignore + if PYDANTIC_V1: + return model.__config__ # type: ignore + return model.model_config def get_model_fields(model: type[pydantic.BaseModel]) -> dict[str, FieldInfo]: - if PYDANTIC_V2: - return model.model_fields - return model.__fields__ # type: ignore + if PYDANTIC_V1: + return model.__fields__ # type: ignore + return model.model_fields def model_copy(model: _ModelT, *, deep: bool = False) -> _ModelT: - if PYDANTIC_V2: - return model.model_copy(deep=deep) - return model.copy(deep=deep) # type: ignore + if PYDANTIC_V1: + return model.copy(deep=deep) # type: ignore + return model.model_copy(deep=deep) def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str: - if PYDANTIC_V2: - return model.model_dump_json(indent=indent) - return model.json(indent=indent) # type: ignore + if PYDANTIC_V1: + return model.json(indent=indent) # type: ignore + return model.model_dump_json(indent=indent) def model_dump( @@ -139,14 +140,14 @@ def model_dump( warnings: bool = True, mode: Literal["json", "python"] = "python", ) -> dict[str, Any]: - if PYDANTIC_V2 or hasattr(model, "model_dump"): + if (not PYDANTIC_V1) or hasattr(model, "model_dump"): return model.model_dump( mode=mode, exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, # warnings are not supported in Pydantic v1 - warnings=warnings if PYDANTIC_V2 else True, + warnings=True if PYDANTIC_V1 else warnings, ) return cast( "dict[str, Any]", @@ -159,9 +160,9 @@ def model_dump( def model_parse(model: type[_ModelT], data: Any) -> _ModelT: - if PYDANTIC_V2: - return model.model_validate(data) - return model.parse_obj(data) # pyright: ignore[reportDeprecated] + if PYDANTIC_V1: + return model.parse_obj(data) # pyright: ignore[reportDeprecated] + return model.model_validate(data) # generic models @@ -170,17 +171,16 @@ def model_parse(model: type[_ModelT], data: Any) -> _ModelT: class GenericModel(pydantic.BaseModel): ... else: - if PYDANTIC_V2: + if PYDANTIC_V1: + import pydantic.generics + + class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel): ... + else: # there no longer needs to be a distinction in v2 but # we still have to create our own subclass to avoid # inconsistent MRO ordering errors class GenericModel(pydantic.BaseModel): ... - else: - import pydantic.generics - - class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel): ... - # cached properties if TYPE_CHECKING: diff --git a/src/kernel/_models.py b/src/kernel/_models.py index 92f7c10b..3a6017ef 100644 --- a/src/kernel/_models.py +++ b/src/kernel/_models.py @@ -50,7 +50,7 @@ strip_annotated_type, ) from ._compat import ( - PYDANTIC_V2, + PYDANTIC_V1, ConfigDict, GenericModel as BaseGenericModel, get_args, @@ -81,11 +81,7 @@ class _ConfigProtocol(Protocol): class BaseModel(pydantic.BaseModel): - if PYDANTIC_V2: - model_config: ClassVar[ConfigDict] = ConfigDict( - extra="allow", defer_build=coerce_boolean(os.environ.get("DEFER_PYDANTIC_BUILD", "true")) - ) - else: + if PYDANTIC_V1: @property @override @@ -95,6 +91,10 @@ def model_fields_set(self) -> set[str]: class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated] extra: Any = pydantic.Extra.allow # type: ignore + else: + model_config: ClassVar[ConfigDict] = ConfigDict( + extra="allow", defer_build=coerce_boolean(os.environ.get("DEFER_PYDANTIC_BUILD", "true")) + ) def to_dict( self, @@ -215,25 +215,25 @@ def construct( # pyright: ignore[reportIncompatibleMethodOverride] if key not in model_fields: parsed = construct_type(value=value, type_=extra_field_type) if extra_field_type is not None else value - if PYDANTIC_V2: - _extra[key] = parsed - else: + if PYDANTIC_V1: _fields_set.add(key) fields_values[key] = parsed + else: + _extra[key] = parsed object.__setattr__(m, "__dict__", fields_values) - if PYDANTIC_V2: - # these properties are copied from Pydantic's `model_construct()` method - object.__setattr__(m, "__pydantic_private__", None) - object.__setattr__(m, "__pydantic_extra__", _extra) - object.__setattr__(m, "__pydantic_fields_set__", _fields_set) - else: + if PYDANTIC_V1: # init_private_attributes() does not exist in v2 m._init_private_attributes() # type: ignore # copied from Pydantic v1's `construct()` method object.__setattr__(m, "__fields_set__", _fields_set) + else: + # these properties are copied from Pydantic's `model_construct()` method + object.__setattr__(m, "__pydantic_private__", None) + object.__setattr__(m, "__pydantic_extra__", _extra) + object.__setattr__(m, "__pydantic_fields_set__", _fields_set) return m @@ -243,7 +243,7 @@ def construct( # pyright: ignore[reportIncompatibleMethodOverride] # although not in practice model_construct = construct - if not PYDANTIC_V2: + if PYDANTIC_V1: # we define aliases for some of the new pydantic v2 methods so # that we can just document these methods without having to specify # a specific pydantic version as some users may not know which @@ -363,10 +363,10 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object: if value is None: return field_get_default(field) - if PYDANTIC_V2: - type_ = field.annotation - else: + if PYDANTIC_V1: type_ = cast(type, field.outer_type_) # type: ignore + else: + type_ = field.annotation # type: ignore if type_ is None: raise RuntimeError(f"Unexpected field type is None for {key}") @@ -375,7 +375,7 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object: def _get_extra_fields_type(cls: type[pydantic.BaseModel]) -> type | None: - if not PYDANTIC_V2: + if PYDANTIC_V1: # TODO return None @@ -628,30 +628,30 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, for variant in get_args(union): variant = strip_annotated_type(variant) if is_basemodel_type(variant): - if PYDANTIC_V2: - field = _extract_field_schema_pv2(variant, discriminator_field_name) - if not field: + if PYDANTIC_V1: + field_info = cast("dict[str, FieldInfo]", variant.__fields__).get(discriminator_field_name) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] + if not field_info: continue # Note: if one variant defines an alias then they all should - discriminator_alias = field.get("serialization_alias") - - field_schema = field["schema"] + discriminator_alias = field_info.alias - if field_schema["type"] == "literal": - for entry in cast("LiteralSchema", field_schema)["expected"]: + if (annotation := getattr(field_info, "annotation", None)) and is_literal_type(annotation): + for entry in get_args(annotation): if isinstance(entry, str): mapping[entry] = variant else: - field_info = cast("dict[str, FieldInfo]", variant.__fields__).get(discriminator_field_name) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] - if not field_info: + field = _extract_field_schema_pv2(variant, discriminator_field_name) + if not field: continue # Note: if one variant defines an alias then they all should - discriminator_alias = field_info.alias + discriminator_alias = field.get("serialization_alias") - if (annotation := getattr(field_info, "annotation", None)) and is_literal_type(annotation): - for entry in get_args(annotation): + field_schema = field["schema"] + + if field_schema["type"] == "literal": + for entry in cast("LiteralSchema", field_schema)["expected"]: if isinstance(entry, str): mapping[entry] = variant @@ -714,7 +714,7 @@ class GenericModel(BaseGenericModel, BaseModel): pass -if PYDANTIC_V2: +if not PYDANTIC_V1: from pydantic import TypeAdapter as _TypeAdapter _CachedTypeAdapter = cast("TypeAdapter[object]", lru_cache(maxsize=None)(_TypeAdapter)) @@ -782,12 +782,12 @@ class FinalRequestOptions(pydantic.BaseModel): json_data: Union[Body, None] = None extra_json: Union[AnyMapping, None] = None - if PYDANTIC_V2: - model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True) - else: + if PYDANTIC_V1: class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated] arbitrary_types_allowed: bool = True + else: + model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True) def get_max_retries(self, max_retries: int) -> int: if isinstance(self.max_retries, NotGiven): @@ -820,9 +820,9 @@ def construct( # type: ignore key: strip_not_given(value) for key, value in values.items() } - if PYDANTIC_V2: - return super().model_construct(_fields_set, **kwargs) - return cast(FinalRequestOptions, super().construct(_fields_set, **kwargs)) # pyright: ignore[reportDeprecated] + if PYDANTIC_V1: + return cast(FinalRequestOptions, super().construct(_fields_set, **kwargs)) # pyright: ignore[reportDeprecated] + return super().model_construct(_fields_set, **kwargs) if not TYPE_CHECKING: # type checkers incorrectly complain about this assignment diff --git a/src/kernel/_utils/__init__.py b/src/kernel/_utils/__init__.py index ca547ce5..dc64e29a 100644 --- a/src/kernel/_utils/__init__.py +++ b/src/kernel/_utils/__init__.py @@ -10,7 +10,6 @@ lru_cache as lru_cache, is_mapping as is_mapping, is_tuple_t as is_tuple_t, - parse_date as parse_date, is_iterable as is_iterable, is_sequence as is_sequence, coerce_float as coerce_float, @@ -23,7 +22,6 @@ coerce_boolean as coerce_boolean, coerce_integer as coerce_integer, file_from_path as file_from_path, - parse_datetime as parse_datetime, strip_not_given as strip_not_given, deepcopy_minimal as deepcopy_minimal, get_async_library as get_async_library, @@ -32,6 +30,13 @@ maybe_coerce_boolean as maybe_coerce_boolean, maybe_coerce_integer as maybe_coerce_integer, ) +from ._compat import ( + get_args as get_args, + is_union as is_union, + get_origin as get_origin, + is_typeddict as is_typeddict, + is_literal_type as is_literal_type, +) from ._typing import ( is_list_type as is_list_type, is_union_type as is_union_type, @@ -56,3 +61,4 @@ function_has_argument as function_has_argument, assert_signatures_in_sync as assert_signatures_in_sync, ) +from ._datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime diff --git a/src/kernel/_utils/_compat.py b/src/kernel/_utils/_compat.py new file mode 100644 index 00000000..dd703233 --- /dev/null +++ b/src/kernel/_utils/_compat.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import sys +import typing_extensions +from typing import Any, Type, Union, Literal, Optional +from datetime import date, datetime +from typing_extensions import get_args as _get_args, get_origin as _get_origin + +from .._types import StrBytesIntFloat +from ._datetime_parse import parse_date as _parse_date, parse_datetime as _parse_datetime + +_LITERAL_TYPES = {Literal, typing_extensions.Literal} + + +def get_args(tp: type[Any]) -> tuple[Any, ...]: + return _get_args(tp) + + +def get_origin(tp: type[Any]) -> type[Any] | None: + return _get_origin(tp) + + +def is_union(tp: Optional[Type[Any]]) -> bool: + if sys.version_info < (3, 10): + return tp is Union # type: ignore[comparison-overlap] + else: + import types + + return tp is Union or tp is types.UnionType + + +def is_typeddict(tp: Type[Any]) -> bool: + return typing_extensions.is_typeddict(tp) + + +def is_literal_type(tp: Type[Any]) -> bool: + return get_origin(tp) in _LITERAL_TYPES + + +def parse_date(value: Union[date, StrBytesIntFloat]) -> date: + return _parse_date(value) + + +def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime: + return _parse_datetime(value) diff --git a/src/kernel/_utils/_datetime_parse.py b/src/kernel/_utils/_datetime_parse.py new file mode 100644 index 00000000..7cb9d9e6 --- /dev/null +++ b/src/kernel/_utils/_datetime_parse.py @@ -0,0 +1,136 @@ +""" +This file contains code from https://github.com/pydantic/pydantic/blob/main/pydantic/v1/datetime_parse.py +without the Pydantic v1 specific errors. +""" + +from __future__ import annotations + +import re +from typing import Dict, Union, Optional +from datetime import date, datetime, timezone, timedelta + +from .._types import StrBytesIntFloat + +date_expr = r"(?P\d{4})-(?P\d{1,2})-(?P\d{1,2})" +time_expr = ( + r"(?P\d{1,2}):(?P\d{1,2})" + r"(?::(?P\d{1,2})(?:\.(?P\d{1,6})\d{0,6})?)?" + r"(?PZ|[+-]\d{2}(?::?\d{2})?)?$" +) + +date_re = re.compile(f"{date_expr}$") +datetime_re = re.compile(f"{date_expr}[T ]{time_expr}") + + +EPOCH = datetime(1970, 1, 1) +# if greater than this, the number is in ms, if less than or equal it's in seconds +# (in seconds this is 11th October 2603, in ms it's 20th August 1970) +MS_WATERSHED = int(2e10) +# slightly more than datetime.max in ns - (datetime.max - EPOCH).total_seconds() * 1e9 +MAX_NUMBER = int(3e20) + + +def _get_numeric(value: StrBytesIntFloat, native_expected_type: str) -> Union[None, int, float]: + if isinstance(value, (int, float)): + return value + try: + return float(value) + except ValueError: + return None + except TypeError: + raise TypeError(f"invalid type; expected {native_expected_type}, string, bytes, int or float") from None + + +def _from_unix_seconds(seconds: Union[int, float]) -> datetime: + if seconds > MAX_NUMBER: + return datetime.max + elif seconds < -MAX_NUMBER: + return datetime.min + + while abs(seconds) > MS_WATERSHED: + seconds /= 1000 + dt = EPOCH + timedelta(seconds=seconds) + return dt.replace(tzinfo=timezone.utc) + + +def _parse_timezone(value: Optional[str]) -> Union[None, int, timezone]: + if value == "Z": + return timezone.utc + elif value is not None: + offset_mins = int(value[-2:]) if len(value) > 3 else 0 + offset = 60 * int(value[1:3]) + offset_mins + if value[0] == "-": + offset = -offset + return timezone(timedelta(minutes=offset)) + else: + return None + + +def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime: + """ + Parse a datetime/int/float/string and return a datetime.datetime. + + This function supports time zone offsets. When the input contains one, + the output uses a timezone with a fixed offset from UTC. + + Raise ValueError if the input is well formatted but not a valid datetime. + Raise ValueError if the input isn't well formatted. + """ + if isinstance(value, datetime): + return value + + number = _get_numeric(value, "datetime") + if number is not None: + return _from_unix_seconds(number) + + if isinstance(value, bytes): + value = value.decode() + + assert not isinstance(value, (float, int)) + + match = datetime_re.match(value) + if match is None: + raise ValueError("invalid datetime format") + + kw = match.groupdict() + if kw["microsecond"]: + kw["microsecond"] = kw["microsecond"].ljust(6, "0") + + tzinfo = _parse_timezone(kw.pop("tzinfo")) + kw_: Dict[str, Union[None, int, timezone]] = {k: int(v) for k, v in kw.items() if v is not None} + kw_["tzinfo"] = tzinfo + + return datetime(**kw_) # type: ignore + + +def parse_date(value: Union[date, StrBytesIntFloat]) -> date: + """ + Parse a date/int/float/string and return a datetime.date. + + Raise ValueError if the input is well formatted but not a valid date. + Raise ValueError if the input isn't well formatted. + """ + if isinstance(value, date): + if isinstance(value, datetime): + return value.date() + else: + return value + + number = _get_numeric(value, "date") + if number is not None: + return _from_unix_seconds(number).date() + + if isinstance(value, bytes): + value = value.decode() + + assert not isinstance(value, (float, int)) + match = date_re.match(value) + if match is None: + raise ValueError("invalid date format") + + kw = {k: int(v) for k, v in match.groupdict().items()} + + try: + return date(**kw) + except ValueError: + raise ValueError("invalid date format") from None diff --git a/src/kernel/_utils/_transform.py b/src/kernel/_utils/_transform.py index f0bcefd4..c19124f0 100644 --- a/src/kernel/_utils/_transform.py +++ b/src/kernel/_utils/_transform.py @@ -19,6 +19,7 @@ is_sequence, ) from .._files import is_base64_file_input +from ._compat import get_origin, is_typeddict from ._typing import ( is_list_type, is_union_type, @@ -29,7 +30,6 @@ is_annotated_type, strip_annotated_type, ) -from .._compat import get_origin, model_dump, is_typeddict _T = TypeVar("_T") @@ -169,6 +169,8 @@ def _transform_recursive( Defaults to the same value as the `annotation` argument. """ + from .._compat import model_dump + if inner_type is None: inner_type = annotation @@ -333,6 +335,8 @@ async def _async_transform_recursive( Defaults to the same value as the `annotation` argument. """ + from .._compat import model_dump + if inner_type is None: inner_type = annotation diff --git a/src/kernel/_utils/_typing.py b/src/kernel/_utils/_typing.py index 845cd6b2..193109f3 100644 --- a/src/kernel/_utils/_typing.py +++ b/src/kernel/_utils/_typing.py @@ -15,7 +15,7 @@ from ._utils import lru_cache from .._types import InheritsGeneric -from .._compat import is_union as _is_union +from ._compat import is_union as _is_union def is_annotated_type(typ: type) -> bool: diff --git a/src/kernel/_utils/_utils.py b/src/kernel/_utils/_utils.py index ea3cf3f2..f0818595 100644 --- a/src/kernel/_utils/_utils.py +++ b/src/kernel/_utils/_utils.py @@ -22,7 +22,6 @@ import sniffio from .._types import NotGiven, FileTypes, NotGivenOr, HeadersLike -from .._compat import parse_date as parse_date, parse_datetime as parse_datetime _T = TypeVar("_T") _TupleT = TypeVar("_TupleT", bound=Tuple[object, ...]) diff --git a/tests/test_models.py b/tests/test_models.py index 72f55a83..ff5955d4 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -8,7 +8,7 @@ from pydantic import Field from kernel._utils import PropertyInfo -from kernel._compat import PYDANTIC_V2, parse_obj, model_dump, model_json +from kernel._compat import PYDANTIC_V1, parse_obj, model_dump, model_json from kernel._models import BaseModel, construct_type @@ -294,12 +294,12 @@ class Model(BaseModel): assert cast(bool, m.foo) is True m = Model.construct(foo={"name": 3}) - if PYDANTIC_V2: - assert isinstance(m.foo, Submodel1) - assert m.foo.name == 3 # type: ignore - else: + if PYDANTIC_V1: assert isinstance(m.foo, Submodel2) assert m.foo.name == "3" + else: + assert isinstance(m.foo, Submodel1) + assert m.foo.name == 3 # type: ignore def test_list_of_unions() -> None: @@ -426,10 +426,10 @@ class Model(BaseModel): expected = datetime(2019, 12, 27, 18, 11, 19, 117000, tzinfo=timezone.utc) - if PYDANTIC_V2: - expected_json = '{"created_at":"2019-12-27T18:11:19.117000Z"}' - else: + if PYDANTIC_V1: expected_json = '{"created_at": "2019-12-27T18:11:19.117000+00:00"}' + else: + expected_json = '{"created_at":"2019-12-27T18:11:19.117000Z"}' model = Model.construct(created_at="2019-12-27T18:11:19.117Z") assert model.created_at == expected @@ -531,7 +531,7 @@ class Model2(BaseModel): assert m4.to_dict(mode="python") == {"created_at": datetime.fromisoformat(time_str)} assert m4.to_dict(mode="json") == {"created_at": time_str} - if not PYDANTIC_V2: + if PYDANTIC_V1: with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): m.to_dict(warnings=False) @@ -556,7 +556,7 @@ class Model(BaseModel): assert m3.model_dump() == {"foo": None} assert m3.model_dump(exclude_none=True) == {} - if not PYDANTIC_V2: + if PYDANTIC_V1: with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"): m.model_dump(round_trip=True) @@ -580,10 +580,10 @@ class Model(BaseModel): assert json.loads(m.to_json()) == {"FOO": "hello"} assert json.loads(m.to_json(use_api_names=False)) == {"foo": "hello"} - if PYDANTIC_V2: - assert m.to_json(indent=None) == '{"FOO":"hello"}' - else: + if PYDANTIC_V1: assert m.to_json(indent=None) == '{"FOO": "hello"}' + else: + assert m.to_json(indent=None) == '{"FOO":"hello"}' m2 = Model() assert json.loads(m2.to_json()) == {} @@ -595,7 +595,7 @@ class Model(BaseModel): assert json.loads(m3.to_json()) == {"FOO": None} assert json.loads(m3.to_json(exclude_none=True)) == {} - if not PYDANTIC_V2: + if PYDANTIC_V1: with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): m.to_json(warnings=False) @@ -622,7 +622,7 @@ class Model(BaseModel): assert json.loads(m3.model_dump_json()) == {"foo": None} assert json.loads(m3.model_dump_json(exclude_none=True)) == {} - if not PYDANTIC_V2: + if PYDANTIC_V1: with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"): m.model_dump_json(round_trip=True) @@ -679,12 +679,12 @@ class B(BaseModel): ) assert isinstance(m, A) assert m.type == "a" - if PYDANTIC_V2: - assert m.data == 100 # type: ignore[comparison-overlap] - else: + if PYDANTIC_V1: # pydantic v1 automatically converts inputs to strings # if the expected type is a str assert m.data == "100" + else: + assert m.data == 100 # type: ignore[comparison-overlap] def test_discriminated_unions_unknown_variant() -> None: @@ -768,12 +768,12 @@ class B(BaseModel): ) assert isinstance(m, A) assert m.foo_type == "a" - if PYDANTIC_V2: - assert m.data == 100 # type: ignore[comparison-overlap] - else: + if PYDANTIC_V1: # pydantic v1 automatically converts inputs to strings # if the expected type is a str assert m.data == "100" + else: + assert m.data == 100 # type: ignore[comparison-overlap] def test_discriminated_unions_overlapping_discriminators_invalid_data() -> None: @@ -833,7 +833,7 @@ class B(BaseModel): assert UnionType.__discriminator__ is discriminator -@pytest.mark.skipif(not PYDANTIC_V2, reason="TypeAliasType is not supported in Pydantic v1") +@pytest.mark.skipif(PYDANTIC_V1, reason="TypeAliasType is not supported in Pydantic v1") def test_type_alias_type() -> None: Alias = TypeAliasType("Alias", str) # pyright: ignore @@ -849,7 +849,7 @@ class Model(BaseModel): assert m.union == "bar" -@pytest.mark.skipif(not PYDANTIC_V2, reason="TypeAliasType is not supported in Pydantic v1") +@pytest.mark.skipif(PYDANTIC_V1, reason="TypeAliasType is not supported in Pydantic v1") def test_field_named_cls() -> None: class Model(BaseModel): cls: str @@ -936,7 +936,7 @@ class Type2(BaseModel): assert isinstance(model.value, InnerType2) -@pytest.mark.skipif(not PYDANTIC_V2, reason="this is only supported in pydantic v2 for now") +@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2 for now") def test_extra_properties() -> None: class Item(BaseModel): prop: int diff --git a/tests/test_transform.py b/tests/test_transform.py index a418f4f1..5f7ab31c 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -15,7 +15,7 @@ parse_datetime, async_transform as _async_transform, ) -from kernel._compat import PYDANTIC_V2 +from kernel._compat import PYDANTIC_V1 from kernel._models import BaseModel _T = TypeVar("_T") @@ -189,7 +189,7 @@ class DateModel(BaseModel): @pytest.mark.asyncio async def test_iso8601_format(use_async: bool) -> None: dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") - tz = "Z" if PYDANTIC_V2 else "+00:00" + tz = "+00:00" if PYDANTIC_V1 else "Z" assert await transform({"foo": dt}, DatetimeDict, use_async) == {"foo": "2023-02-23T14:16:36.337692+00:00"} # type: ignore[comparison-overlap] assert await transform(DatetimeModel(foo=dt), Any, use_async) == {"foo": "2023-02-23T14:16:36.337692" + tz} # type: ignore[comparison-overlap] @@ -297,11 +297,11 @@ async def test_pydantic_unknown_field(use_async: bool) -> None: @pytest.mark.asyncio async def test_pydantic_mismatched_types(use_async: bool) -> None: model = MyModel.construct(foo=True) - if PYDANTIC_V2: + if PYDANTIC_V1: + params = await transform(model, Any, use_async) + else: with pytest.warns(UserWarning): params = await transform(model, Any, use_async) - else: - params = await transform(model, Any, use_async) assert cast(Any, params) == {"foo": True} @@ -309,11 +309,11 @@ async def test_pydantic_mismatched_types(use_async: bool) -> None: @pytest.mark.asyncio async def test_pydantic_mismatched_object_type(use_async: bool) -> None: model = MyModel.construct(foo=MyModel.construct(hello="world")) - if PYDANTIC_V2: + if PYDANTIC_V1: + params = await transform(model, Any, use_async) + else: with pytest.warns(UserWarning): params = await transform(model, Any, use_async) - else: - params = await transform(model, Any, use_async) assert cast(Any, params) == {"foo": {"hello": "world"}} diff --git a/tests/test_utils/test_datetime_parse.py b/tests/test_utils/test_datetime_parse.py new file mode 100644 index 00000000..f6265329 --- /dev/null +++ b/tests/test_utils/test_datetime_parse.py @@ -0,0 +1,110 @@ +""" +Copied from https://github.com/pydantic/pydantic/blob/v1.10.22/tests/test_datetime_parse.py +with modifications so it works without pydantic v1 imports. +""" + +from typing import Type, Union +from datetime import date, datetime, timezone, timedelta + +import pytest + +from kernel._utils import parse_date, parse_datetime + + +def create_tz(minutes: int) -> timezone: + return timezone(timedelta(minutes=minutes)) + + +@pytest.mark.parametrize( + "value,result", + [ + # Valid inputs + ("1494012444.883309", date(2017, 5, 5)), + (b"1494012444.883309", date(2017, 5, 5)), + (1_494_012_444.883_309, date(2017, 5, 5)), + ("1494012444", date(2017, 5, 5)), + (1_494_012_444, date(2017, 5, 5)), + (0, date(1970, 1, 1)), + ("2012-04-23", date(2012, 4, 23)), + (b"2012-04-23", date(2012, 4, 23)), + ("2012-4-9", date(2012, 4, 9)), + (date(2012, 4, 9), date(2012, 4, 9)), + (datetime(2012, 4, 9, 12, 15), date(2012, 4, 9)), + # Invalid inputs + ("x20120423", ValueError), + ("2012-04-56", ValueError), + (19_999_999_999, date(2603, 10, 11)), # just before watershed + (20_000_000_001, date(1970, 8, 20)), # just after watershed + (1_549_316_052, date(2019, 2, 4)), # nowish in s + (1_549_316_052_104, date(2019, 2, 4)), # nowish in ms + (1_549_316_052_104_324, date(2019, 2, 4)), # nowish in μs + (1_549_316_052_104_324_096, date(2019, 2, 4)), # nowish in ns + ("infinity", date(9999, 12, 31)), + ("inf", date(9999, 12, 31)), + (float("inf"), date(9999, 12, 31)), + ("infinity ", date(9999, 12, 31)), + (int("1" + "0" * 100), date(9999, 12, 31)), + (1e1000, date(9999, 12, 31)), + ("-infinity", date(1, 1, 1)), + ("-inf", date(1, 1, 1)), + ("nan", ValueError), + ], +) +def test_date_parsing(value: Union[str, bytes, int, float], result: Union[date, Type[Exception]]) -> None: + if type(result) == type and issubclass(result, Exception): # pyright: ignore[reportUnnecessaryIsInstance] + with pytest.raises(result): + parse_date(value) + else: + assert parse_date(value) == result + + +@pytest.mark.parametrize( + "value,result", + [ + # Valid inputs + # values in seconds + ("1494012444.883309", datetime(2017, 5, 5, 19, 27, 24, 883_309, tzinfo=timezone.utc)), + (1_494_012_444.883_309, datetime(2017, 5, 5, 19, 27, 24, 883_309, tzinfo=timezone.utc)), + ("1494012444", datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)), + (b"1494012444", datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)), + (1_494_012_444, datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)), + # values in ms + ("1494012444000.883309", datetime(2017, 5, 5, 19, 27, 24, 883, tzinfo=timezone.utc)), + ("-1494012444000.883309", datetime(1922, 8, 29, 4, 32, 35, 999117, tzinfo=timezone.utc)), + (1_494_012_444_000, datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)), + ("2012-04-23T09:15:00", datetime(2012, 4, 23, 9, 15)), + ("2012-4-9 4:8:16", datetime(2012, 4, 9, 4, 8, 16)), + ("2012-04-23T09:15:00Z", datetime(2012, 4, 23, 9, 15, 0, 0, timezone.utc)), + ("2012-4-9 4:8:16-0320", datetime(2012, 4, 9, 4, 8, 16, 0, create_tz(-200))), + ("2012-04-23T10:20:30.400+02:30", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(150))), + ("2012-04-23T10:20:30.400+02", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(120))), + ("2012-04-23T10:20:30.400-02", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(-120))), + (b"2012-04-23T10:20:30.400-02", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(-120))), + (datetime(2017, 5, 5), datetime(2017, 5, 5)), + (0, datetime(1970, 1, 1, 0, 0, 0, tzinfo=timezone.utc)), + # Invalid inputs + ("x20120423091500", ValueError), + ("2012-04-56T09:15:90", ValueError), + ("2012-04-23T11:05:00-25:00", ValueError), + (19_999_999_999, datetime(2603, 10, 11, 11, 33, 19, tzinfo=timezone.utc)), # just before watershed + (20_000_000_001, datetime(1970, 8, 20, 11, 33, 20, 1000, tzinfo=timezone.utc)), # just after watershed + (1_549_316_052, datetime(2019, 2, 4, 21, 34, 12, 0, tzinfo=timezone.utc)), # nowish in s + (1_549_316_052_104, datetime(2019, 2, 4, 21, 34, 12, 104_000, tzinfo=timezone.utc)), # nowish in ms + (1_549_316_052_104_324, datetime(2019, 2, 4, 21, 34, 12, 104_324, tzinfo=timezone.utc)), # nowish in μs + (1_549_316_052_104_324_096, datetime(2019, 2, 4, 21, 34, 12, 104_324, tzinfo=timezone.utc)), # nowish in ns + ("infinity", datetime(9999, 12, 31, 23, 59, 59, 999999)), + ("inf", datetime(9999, 12, 31, 23, 59, 59, 999999)), + ("inf ", datetime(9999, 12, 31, 23, 59, 59, 999999)), + (1e50, datetime(9999, 12, 31, 23, 59, 59, 999999)), + (float("inf"), datetime(9999, 12, 31, 23, 59, 59, 999999)), + ("-infinity", datetime(1, 1, 1, 0, 0)), + ("-inf", datetime(1, 1, 1, 0, 0)), + ("nan", ValueError), + ], +) +def test_datetime_parsing(value: Union[str, bytes, int, float], result: Union[datetime, Type[Exception]]) -> None: + if type(result) == type and issubclass(result, Exception): # pyright: ignore[reportUnnecessaryIsInstance] + with pytest.raises(result): + parse_datetime(value) + else: + assert parse_datetime(value) == result diff --git a/tests/utils.py b/tests/utils.py index 3e90233d..3147457a 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -19,7 +19,7 @@ is_annotated_type, is_type_alias_type, ) -from kernel._compat import PYDANTIC_V2, field_outer_type, get_model_fields +from kernel._compat import PYDANTIC_V1, field_outer_type, get_model_fields from kernel._models import BaseModel BaseModelT = TypeVar("BaseModelT", bound=BaseModel) @@ -28,12 +28,12 @@ def assert_matches_model(model: type[BaseModelT], value: BaseModelT, *, path: list[str]) -> bool: for name, field in get_model_fields(model).items(): field_value = getattr(value, name) - if PYDANTIC_V2: - allow_none = False - else: + if PYDANTIC_V1: # in v1 nullability was structured differently # https://docs.pydantic.dev/2.0/migration/#required-optional-and-nullable-fields allow_none = getattr(field, "allow_none", False) + else: + allow_none = False assert_matches_type( field_outer_type(field), From 6882e5deff01dd9c98ac0ad45edb6ff1ca871594 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 15:09:30 +0000 Subject: [PATCH 155/448] feat(api): adding support for browser profiles --- .stats.yml | 8 +- api.md | 17 + src/kernel/_client.py | 10 +- src/kernel/resources/__init__.py | 14 + src/kernel/resources/browsers/browsers.py | 12 + src/kernel/resources/profiles.py | 474 ++++++++++++++++++ src/kernel/types/__init__.py | 3 + src/kernel/types/browser_create_params.py | 26 +- src/kernel/types/browser_create_response.py | 4 + src/kernel/types/browser_list_response.py | 4 + src/kernel/types/browser_retrieve_response.py | 4 + src/kernel/types/profile.py | 25 + src/kernel/types/profile_create_params.py | 12 + src/kernel/types/profile_list_response.py | 10 + tests/api_resources/test_browsers.py | 10 + tests/api_resources/test_profiles.py | 428 ++++++++++++++++ 16 files changed, 1055 insertions(+), 6 deletions(-) create mode 100644 src/kernel/resources/profiles.py create mode 100644 src/kernel/types/profile.py create mode 100644 src/kernel/types/profile_create_params.py create mode 100644 src/kernel/types/profile_list_response.py create mode 100644 tests/api_resources/test_profiles.py diff --git a/.stats.yml b/.stats.yml index b791078d..6ac19baf 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 41 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-a7c1df5070fe59642d7a1f168aa902a468227752bfc930cbf38930f7c205dbb6.yml -openapi_spec_hash: eab65e39aef4f0a0952b82adeecf6b5b -config_hash: 5de78bc29ac060562575cb54bb26826c +configured_endpoints: 46 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-e98d46c55826cdf541a9ee0df04ce92806ac6d4d92957ae79f897270b7d85b23.yml +openapi_spec_hash: 8a1af54fc0a4417165b8a52e6354b685 +config_hash: 043ddc54629c6d8b889123770cb4769f diff --git a/api.md b/api.md index 7e07a7b0..78295850 100644 --- a/api.md +++ b/api.md @@ -66,6 +66,7 @@ Types: ```python from kernel.types import ( BrowserPersistence, + Profile, BrowserCreateResponse, BrowserRetrieveResponse, BrowserListResponse, @@ -161,3 +162,19 @@ Methods: Methods: - client.browsers.logs.stream(id, \*\*params) -> LogEvent + +# Profiles + +Types: + +```python +from kernel.types import ProfileListResponse +``` + +Methods: + +- client.profiles.create(\*\*params) -> Profile +- client.profiles.retrieve(id_or_name) -> Profile +- client.profiles.list() -> ProfileListResponse +- client.profiles.delete(id_or_name) -> None +- client.profiles.download(id_or_name) -> BinaryAPIResponse diff --git a/src/kernel/_client.py b/src/kernel/_client.py index a0b9ec29..3b4235cd 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -21,7 +21,7 @@ ) from ._utils import is_given, get_async_library from ._version import __version__ -from .resources import apps, deployments, invocations +from .resources import apps, profiles, deployments, invocations from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import KernelError, APIStatusError from ._base_client import ( @@ -54,6 +54,7 @@ class Kernel(SyncAPIClient): apps: apps.AppsResource invocations: invocations.InvocationsResource browsers: browsers.BrowsersResource + profiles: profiles.ProfilesResource with_raw_response: KernelWithRawResponse with_streaming_response: KernelWithStreamedResponse @@ -139,6 +140,7 @@ def __init__( self.apps = apps.AppsResource(self) self.invocations = invocations.InvocationsResource(self) self.browsers = browsers.BrowsersResource(self) + self.profiles = profiles.ProfilesResource(self) self.with_raw_response = KernelWithRawResponse(self) self.with_streaming_response = KernelWithStreamedResponse(self) @@ -254,6 +256,7 @@ class AsyncKernel(AsyncAPIClient): apps: apps.AsyncAppsResource invocations: invocations.AsyncInvocationsResource browsers: browsers.AsyncBrowsersResource + profiles: profiles.AsyncProfilesResource with_raw_response: AsyncKernelWithRawResponse with_streaming_response: AsyncKernelWithStreamedResponse @@ -339,6 +342,7 @@ def __init__( self.apps = apps.AsyncAppsResource(self) self.invocations = invocations.AsyncInvocationsResource(self) self.browsers = browsers.AsyncBrowsersResource(self) + self.profiles = profiles.AsyncProfilesResource(self) self.with_raw_response = AsyncKernelWithRawResponse(self) self.with_streaming_response = AsyncKernelWithStreamedResponse(self) @@ -455,6 +459,7 @@ def __init__(self, client: Kernel) -> None: self.apps = apps.AppsResourceWithRawResponse(client.apps) self.invocations = invocations.InvocationsResourceWithRawResponse(client.invocations) self.browsers = browsers.BrowsersResourceWithRawResponse(client.browsers) + self.profiles = profiles.ProfilesResourceWithRawResponse(client.profiles) class AsyncKernelWithRawResponse: @@ -463,6 +468,7 @@ def __init__(self, client: AsyncKernel) -> None: self.apps = apps.AsyncAppsResourceWithRawResponse(client.apps) self.invocations = invocations.AsyncInvocationsResourceWithRawResponse(client.invocations) self.browsers = browsers.AsyncBrowsersResourceWithRawResponse(client.browsers) + self.profiles = profiles.AsyncProfilesResourceWithRawResponse(client.profiles) class KernelWithStreamedResponse: @@ -471,6 +477,7 @@ def __init__(self, client: Kernel) -> None: self.apps = apps.AppsResourceWithStreamingResponse(client.apps) self.invocations = invocations.InvocationsResourceWithStreamingResponse(client.invocations) self.browsers = browsers.BrowsersResourceWithStreamingResponse(client.browsers) + self.profiles = profiles.ProfilesResourceWithStreamingResponse(client.profiles) class AsyncKernelWithStreamedResponse: @@ -479,6 +486,7 @@ def __init__(self, client: AsyncKernel) -> None: self.apps = apps.AsyncAppsResourceWithStreamingResponse(client.apps) self.invocations = invocations.AsyncInvocationsResourceWithStreamingResponse(client.invocations) self.browsers = browsers.AsyncBrowsersResourceWithStreamingResponse(client.browsers) + self.profiles = profiles.AsyncProfilesResourceWithStreamingResponse(client.profiles) Client = Kernel diff --git a/src/kernel/resources/__init__.py b/src/kernel/resources/__init__.py index 3b6a4d62..964da373 100644 --- a/src/kernel/resources/__init__.py +++ b/src/kernel/resources/__init__.py @@ -16,6 +16,14 @@ BrowsersResourceWithStreamingResponse, AsyncBrowsersResourceWithStreamingResponse, ) +from .profiles import ( + ProfilesResource, + AsyncProfilesResource, + ProfilesResourceWithRawResponse, + AsyncProfilesResourceWithRawResponse, + ProfilesResourceWithStreamingResponse, + AsyncProfilesResourceWithStreamingResponse, +) from .deployments import ( DeploymentsResource, AsyncDeploymentsResource, @@ -58,4 +66,10 @@ "AsyncBrowsersResourceWithRawResponse", "BrowsersResourceWithStreamingResponse", "AsyncBrowsersResourceWithStreamingResponse", + "ProfilesResource", + "AsyncProfilesResource", + "ProfilesResourceWithRawResponse", + "AsyncProfilesResourceWithRawResponse", + "ProfilesResourceWithStreamingResponse", + "AsyncProfilesResourceWithStreamingResponse", ] diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 80afc60d..7394f21a 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -98,6 +98,7 @@ def create( headless: bool | NotGiven = NOT_GIVEN, invocation_id: str | NotGiven = NOT_GIVEN, persistence: BrowserPersistenceParam | NotGiven = NOT_GIVEN, + profile: browser_create_params.Profile | NotGiven = NOT_GIVEN, stealth: bool | NotGiven = NOT_GIVEN, timeout_seconds: int | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -118,6 +119,10 @@ def create( persistence: Optional persistence configuration for the browser session. + profile: Profile selection for the browser session. Provide either id or name. If + specified, the matching profile will be loaded into the browser session. + Profiles must be created beforehand. + stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. @@ -140,6 +145,7 @@ def create( "headless": headless, "invocation_id": invocation_id, "persistence": persistence, + "profile": profile, "stealth": stealth, "timeout_seconds": timeout_seconds, }, @@ -318,6 +324,7 @@ async def create( headless: bool | NotGiven = NOT_GIVEN, invocation_id: str | NotGiven = NOT_GIVEN, persistence: BrowserPersistenceParam | NotGiven = NOT_GIVEN, + profile: browser_create_params.Profile | NotGiven = NOT_GIVEN, stealth: bool | NotGiven = NOT_GIVEN, timeout_seconds: int | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -338,6 +345,10 @@ async def create( persistence: Optional persistence configuration for the browser session. + profile: Profile selection for the browser session. Provide either id or name. If + specified, the matching profile will be loaded into the browser session. + Profiles must be created beforehand. + stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. @@ -360,6 +371,7 @@ async def create( "headless": headless, "invocation_id": invocation_id, "persistence": persistence, + "profile": profile, "stealth": stealth, "timeout_seconds": timeout_seconds, }, diff --git a/src/kernel/resources/profiles.py b/src/kernel/resources/profiles.py new file mode 100644 index 00000000..0818cdc9 --- /dev/null +++ b/src/kernel/resources/profiles.py @@ -0,0 +1,474 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..types import profile_create_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 ( + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + to_custom_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_custom_streamed_response_wrapper, + async_to_custom_raw_response_wrapper, + async_to_custom_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.profile import Profile +from ..types.profile_list_response import ProfileListResponse + +__all__ = ["ProfilesResource", "AsyncProfilesResource"] + + +class ProfilesResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> ProfilesResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return ProfilesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ProfilesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return ProfilesResourceWithStreamingResponse(self) + + def create( + self, + *, + name: 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, + ) -> Profile: + """ + Create a browser profile that can be used to load state into future browser + sessions. + + Args: + name: Optional name of the profile. Must be unique within the organization. + + 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( + "/profiles", + body=maybe_transform({"name": name}, profile_create_params.ProfileCreateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Profile, + ) + + def retrieve( + self, + id_or_name: 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, + ) -> Profile: + """ + Retrieve details for a single profile by its ID or name. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") + return self._get( + f"/profiles/{id_or_name}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Profile, + ) + + def list( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ProfileListResponse: + """List profiles with optional filtering and pagination.""" + return self._get( + "/profiles", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProfileListResponse, + ) + + def delete( + self, + id_or_name: 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 profile by its ID or by its name. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + f"/profiles/{id_or_name}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + def download( + self, + id_or_name: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BinaryAPIResponse: + """Download the profile. + + Profiles are JSON files containing the pieces of state + that we save. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + return self._get( + f"/profiles/{id_or_name}/download", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BinaryAPIResponse, + ) + + +class AsyncProfilesResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncProfilesResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncProfilesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncProfilesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return AsyncProfilesResourceWithStreamingResponse(self) + + async def create( + self, + *, + name: 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, + ) -> Profile: + """ + Create a browser profile that can be used to load state into future browser + sessions. + + Args: + name: Optional name of the profile. Must be unique within the organization. + + 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( + "/profiles", + body=await async_maybe_transform({"name": name}, profile_create_params.ProfileCreateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Profile, + ) + + async def retrieve( + self, + id_or_name: 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, + ) -> Profile: + """ + Retrieve details for a single profile by its ID or name. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") + return await self._get( + f"/profiles/{id_or_name}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Profile, + ) + + async def list( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ProfileListResponse: + """List profiles with optional filtering and pagination.""" + return await self._get( + "/profiles", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProfileListResponse, + ) + + async def delete( + self, + id_or_name: 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 profile by its ID or by its name. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + f"/profiles/{id_or_name}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + async def download( + self, + id_or_name: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncBinaryAPIResponse: + """Download the profile. + + Profiles are JSON files containing the pieces of state + that we save. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + return await self._get( + f"/profiles/{id_or_name}/download", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AsyncBinaryAPIResponse, + ) + + +class ProfilesResourceWithRawResponse: + def __init__(self, profiles: ProfilesResource) -> None: + self._profiles = profiles + + self.create = to_raw_response_wrapper( + profiles.create, + ) + self.retrieve = to_raw_response_wrapper( + profiles.retrieve, + ) + self.list = to_raw_response_wrapper( + profiles.list, + ) + self.delete = to_raw_response_wrapper( + profiles.delete, + ) + self.download = to_custom_raw_response_wrapper( + profiles.download, + BinaryAPIResponse, + ) + + +class AsyncProfilesResourceWithRawResponse: + def __init__(self, profiles: AsyncProfilesResource) -> None: + self._profiles = profiles + + self.create = async_to_raw_response_wrapper( + profiles.create, + ) + self.retrieve = async_to_raw_response_wrapper( + profiles.retrieve, + ) + self.list = async_to_raw_response_wrapper( + profiles.list, + ) + self.delete = async_to_raw_response_wrapper( + profiles.delete, + ) + self.download = async_to_custom_raw_response_wrapper( + profiles.download, + AsyncBinaryAPIResponse, + ) + + +class ProfilesResourceWithStreamingResponse: + def __init__(self, profiles: ProfilesResource) -> None: + self._profiles = profiles + + self.create = to_streamed_response_wrapper( + profiles.create, + ) + self.retrieve = to_streamed_response_wrapper( + profiles.retrieve, + ) + self.list = to_streamed_response_wrapper( + profiles.list, + ) + self.delete = to_streamed_response_wrapper( + profiles.delete, + ) + self.download = to_custom_streamed_response_wrapper( + profiles.download, + StreamedBinaryAPIResponse, + ) + + +class AsyncProfilesResourceWithStreamingResponse: + def __init__(self, profiles: AsyncProfilesResource) -> None: + self._profiles = profiles + + self.create = async_to_streamed_response_wrapper( + profiles.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + profiles.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + profiles.list, + ) + self.delete = async_to_streamed_response_wrapper( + profiles.delete, + ) + self.download = async_to_custom_streamed_response_wrapper( + profiles.download, + AsyncStreamedBinaryAPIResponse, + ) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index c89d7d5d..3c17469d 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -10,12 +10,15 @@ ErrorDetail as ErrorDetail, HeartbeatEvent as HeartbeatEvent, ) +from .profile import Profile as Profile from .app_list_params import AppListParams as AppListParams from .app_list_response import AppListResponse as AppListResponse from .browser_persistence import BrowserPersistence as BrowserPersistence from .browser_create_params import BrowserCreateParams as BrowserCreateParams from .browser_delete_params import BrowserDeleteParams as BrowserDeleteParams from .browser_list_response import BrowserListResponse as BrowserListResponse +from .profile_create_params import ProfileCreateParams as ProfileCreateParams +from .profile_list_response import ProfileListResponse as ProfileListResponse from .deployment_list_params import DeploymentListParams as DeploymentListParams from .deployment_state_event import DeploymentStateEvent as DeploymentStateEvent from .invocation_state_event import InvocationStateEvent as InvocationStateEvent diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index 140dac02..b9bb3303 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -6,7 +6,7 @@ from .browser_persistence_param import BrowserPersistenceParam -__all__ = ["BrowserCreateParams"] +__all__ = ["BrowserCreateParams", "Profile"] class BrowserCreateParams(TypedDict, total=False): @@ -22,6 +22,13 @@ class BrowserCreateParams(TypedDict, total=False): persistence: BrowserPersistenceParam """Optional persistence configuration for the browser session.""" + profile: Profile + """Profile selection for the browser session. + + Provide either id or name. If specified, the matching profile will be loaded + into the browser session. Profiles must be created beforehand. + """ + stealth: bool """ If true, launches the browser in stealth mode to reduce detection by anti-bot @@ -34,3 +41,20 @@ class BrowserCreateParams(TypedDict, total=False): Only applicable to non-persistent browsers. Activity includes CDP connections and live view connections. Defaults to 60 seconds. """ + + +class Profile(TypedDict, total=False): + id: str + """Profile ID to load for this browser session""" + + name: str + """Profile name to load for this browser session (instead of id). + + Must be 1-255 characters, using letters, numbers, dots, underscores, or hyphens. + """ + + save_changes: bool + """ + If true, save changes made during the session back to the profile when the + session ends. + """ diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index 145b267d..a1bc00ed 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -3,6 +3,7 @@ from typing import Optional from datetime import datetime +from .profile import Profile from .._models import BaseModel from .browser_persistence import BrowserPersistence @@ -36,3 +37,6 @@ class BrowserCreateResponse(BaseModel): persistence: Optional[BrowserPersistence] = None """Optional persistence configuration for the browser session.""" + + profile: Optional[Profile] = None + """Browser profile metadata.""" diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index b0157a1f..08ddbd54 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -4,6 +4,7 @@ from datetime import datetime from typing_extensions import TypeAlias +from .profile import Profile from .._models import BaseModel from .browser_persistence import BrowserPersistence @@ -38,5 +39,8 @@ class BrowserListResponseItem(BaseModel): persistence: Optional[BrowserPersistence] = None """Optional persistence configuration for the browser session.""" + profile: Optional[Profile] = None + """Browser profile metadata.""" + BrowserListResponse: TypeAlias = List[BrowserListResponseItem] diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index f0ab7a5f..fc4c8396 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -3,6 +3,7 @@ from typing import Optional from datetime import datetime +from .profile import Profile from .._models import BaseModel from .browser_persistence import BrowserPersistence @@ -36,3 +37,6 @@ class BrowserRetrieveResponse(BaseModel): persistence: Optional[BrowserPersistence] = None """Optional persistence configuration for the browser session.""" + + profile: Optional[Profile] = None + """Browser profile metadata.""" diff --git a/src/kernel/types/profile.py b/src/kernel/types/profile.py new file mode 100644 index 00000000..3ec58903 --- /dev/null +++ b/src/kernel/types/profile.py @@ -0,0 +1,25 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime + +from .._models import BaseModel + +__all__ = ["Profile"] + + +class Profile(BaseModel): + id: str + """Unique identifier for the profile""" + + created_at: datetime + """Timestamp when the profile was created""" + + last_used_at: Optional[datetime] = None + """Timestamp when the profile was last used""" + + name: Optional[str] = None + """Optional, easier-to-reference name for the profile""" + + updated_at: Optional[datetime] = None + """Timestamp when the profile was last updated""" diff --git a/src/kernel/types/profile_create_params.py b/src/kernel/types/profile_create_params.py new file mode 100644 index 00000000..0b2b12ae --- /dev/null +++ b/src/kernel/types/profile_create_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["ProfileCreateParams"] + + +class ProfileCreateParams(TypedDict, total=False): + name: str + """Optional name of the profile. Must be unique within the organization.""" diff --git a/src/kernel/types/profile_list_response.py b/src/kernel/types/profile_list_response.py new file mode 100644 index 00000000..24b2744c --- /dev/null +++ b/src/kernel/types/profile_list_response.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List +from typing_extensions import TypeAlias + +from .profile import Profile + +__all__ = ["ProfileListResponse"] + +ProfileListResponse: TypeAlias = List[Profile] diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index 6f9437f5..be4344f4 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -34,6 +34,11 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: headless=False, invocation_id="rr33xuugxj9h0bkf1rdt2bet", persistence={"id": "my-awesome-browser-for-user-1234"}, + profile={ + "id": "id", + "name": "name", + "save_changes": True, + }, stealth=True, timeout_seconds=0, ) @@ -226,6 +231,11 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> headless=False, invocation_id="rr33xuugxj9h0bkf1rdt2bet", persistence={"id": "my-awesome-browser-for-user-1234"}, + profile={ + "id": "id", + "name": "name", + "save_changes": True, + }, stealth=True, timeout_seconds=0, ) diff --git a/tests/api_resources/test_profiles.py b/tests/api_resources/test_profiles.py new file mode 100644 index 00000000..6c978558 --- /dev/null +++ b/tests/api_resources/test_profiles.py @@ -0,0 +1,428 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import httpx +import pytest +from respx import MockRouter + +from kernel import Kernel, AsyncKernel +from tests.utils import assert_matches_type +from kernel.types import Profile, ProfileListResponse +from kernel._response import ( + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestProfiles: + 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: Kernel) -> None: + profile = client.profiles.create() + assert_matches_type(Profile, profile, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_create_with_all_params(self, client: Kernel) -> None: + profile = client.profiles.create( + name="name", + ) + assert_matches_type(Profile, profile, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_create(self, client: Kernel) -> None: + response = client.profiles.with_raw_response.create() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + profile = response.parse() + assert_matches_type(Profile, profile, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_create(self, client: Kernel) -> None: + with client.profiles.with_streaming_response.create() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + profile = response.parse() + assert_matches_type(Profile, 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: Kernel) -> None: + profile = client.profiles.retrieve( + "id_or_name", + ) + assert_matches_type(Profile, profile, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: Kernel) -> None: + response = client.profiles.with_raw_response.retrieve( + "id_or_name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + profile = response.parse() + assert_matches_type(Profile, profile, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: Kernel) -> None: + with client.profiles.with_streaming_response.retrieve( + "id_or_name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + profile = response.parse() + assert_matches_type(Profile, 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: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): + client.profiles.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_list(self, client: Kernel) -> None: + profile = client.profiles.list() + assert_matches_type(ProfileListResponse, profile, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_list(self, client: Kernel) -> None: + response = client.profiles.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + profile = response.parse() + assert_matches_type(ProfileListResponse, profile, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_list(self, client: Kernel) -> None: + with client.profiles.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + profile = response.parse() + assert_matches_type(ProfileListResponse, 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: Kernel) -> None: + profile = client.profiles.delete( + "id_or_name", + ) + assert profile is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_delete(self, client: Kernel) -> None: + response = client.profiles.with_raw_response.delete( + "id_or_name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + profile = response.parse() + assert profile is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_delete(self, client: Kernel) -> None: + with client.profiles.with_streaming_response.delete( + "id_or_name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + profile = response.parse() + assert 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: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): + client.profiles.with_raw_response.delete( + "", + ) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_method_download(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/profiles/id_or_name/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + profile = client.profiles.download( + "id_or_name", + ) + assert profile.is_closed + assert profile.json() == {"foo": "bar"} + assert cast(Any, profile.is_closed) is True + assert isinstance(profile, BinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_raw_response_download(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/profiles/id_or_name/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + profile = client.profiles.with_raw_response.download( + "id_or_name", + ) + + assert profile.is_closed is True + assert profile.http_request.headers.get("X-Stainless-Lang") == "python" + assert profile.json() == {"foo": "bar"} + assert isinstance(profile, BinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_streaming_response_download(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/profiles/id_or_name/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + with client.profiles.with_streaming_response.download( + "id_or_name", + ) as profile: + assert not profile.is_closed + assert profile.http_request.headers.get("X-Stainless-Lang") == "python" + + assert profile.json() == {"foo": "bar"} + assert cast(Any, profile.is_closed) is True + assert isinstance(profile, StreamedBinaryAPIResponse) + + assert cast(Any, profile.is_closed) is True + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_path_params_download(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): + client.profiles.with_raw_response.download( + "", + ) + + +class TestAsyncProfiles: + 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: AsyncKernel) -> None: + profile = await async_client.profiles.create() + assert_matches_type(Profile, profile, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: + profile = await async_client.profiles.create( + name="name", + ) + assert_matches_type(Profile, profile, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_create(self, async_client: AsyncKernel) -> None: + response = await async_client.profiles.with_raw_response.create() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + profile = await response.parse() + assert_matches_type(Profile, profile, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: + async with async_client.profiles.with_streaming_response.create() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + profile = await response.parse() + assert_matches_type(Profile, 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: AsyncKernel) -> None: + profile = await async_client.profiles.retrieve( + "id_or_name", + ) + assert_matches_type(Profile, profile, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: + response = await async_client.profiles.with_raw_response.retrieve( + "id_or_name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + profile = await response.parse() + assert_matches_type(Profile, profile, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: + async with async_client.profiles.with_streaming_response.retrieve( + "id_or_name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + profile = await response.parse() + assert_matches_type(Profile, 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: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): + await async_client.profiles.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncKernel) -> None: + profile = await async_client.profiles.list() + assert_matches_type(ProfileListResponse, profile, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncKernel) -> None: + response = await async_client.profiles.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + profile = await response.parse() + assert_matches_type(ProfileListResponse, profile, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: + async with async_client.profiles.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + profile = await response.parse() + assert_matches_type(ProfileListResponse, 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: AsyncKernel) -> None: + profile = await async_client.profiles.delete( + "id_or_name", + ) + assert profile is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: + response = await async_client.profiles.with_raw_response.delete( + "id_or_name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + profile = await response.parse() + assert profile is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None: + async with async_client.profiles.with_streaming_response.delete( + "id_or_name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + profile = await response.parse() + assert 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: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): + await async_client.profiles.with_raw_response.delete( + "", + ) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_method_download(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/profiles/id_or_name/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + profile = await async_client.profiles.download( + "id_or_name", + ) + assert profile.is_closed + assert await profile.json() == {"foo": "bar"} + assert cast(Any, profile.is_closed) is True + assert isinstance(profile, AsyncBinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_raw_response_download(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/profiles/id_or_name/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + profile = await async_client.profiles.with_raw_response.download( + "id_or_name", + ) + + assert profile.is_closed is True + assert profile.http_request.headers.get("X-Stainless-Lang") == "python" + assert await profile.json() == {"foo": "bar"} + assert isinstance(profile, AsyncBinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_streaming_response_download(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/profiles/id_or_name/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + async with async_client.profiles.with_streaming_response.download( + "id_or_name", + ) as profile: + assert not profile.is_closed + assert profile.http_request.headers.get("X-Stainless-Lang") == "python" + + assert await profile.json() == {"foo": "bar"} + assert cast(Any, profile.is_closed) is True + assert isinstance(profile, AsyncStreamedBinaryAPIResponse) + + assert cast(Any, profile.is_closed) is True + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_path_params_download(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): + await async_client.profiles.with_raw_response.download( + "", + ) From 5e492689a8964ec0aed0d0b1fc6979e6a0a43eb7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 15:18:25 +0000 Subject: [PATCH 156/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 091cfb12..f7014c35 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.10.0" + ".": "0.11.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 3cc0fb34..f731ab32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.10.0" +version = "0.11.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index db4afd4b..1caec76c 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.10.0" # x-release-please-version +__version__ = "0.11.0" # x-release-please-version From d074616e796be7b304748163bb7741815528d3af Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 03:38:14 +0000 Subject: [PATCH 157/448] chore(internal): move mypy configurations to `pyproject.toml` file --- mypy.ini | 50 ------------------------------------------------ pyproject.toml | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 50 deletions(-) delete mode 100644 mypy.ini diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 0745431c..00000000 --- a/mypy.ini +++ /dev/null @@ -1,50 +0,0 @@ -[mypy] -pretty = True -show_error_codes = True - -# Exclude _files.py because mypy isn't smart enough to apply -# the correct type narrowing and as this is an internal module -# it's fine to just use Pyright. -# -# We also exclude our `tests` as mypy doesn't always infer -# types correctly and Pyright will still catch any type errors. -exclude = ^(src/kernel/_files\.py|_dev/.*\.py|tests/.*)$ - -strict_equality = True -implicit_reexport = True -check_untyped_defs = True -no_implicit_optional = True - -warn_return_any = True -warn_unreachable = True -warn_unused_configs = True - -# Turn these options off as it could cause conflicts -# with the Pyright options. -warn_unused_ignores = False -warn_redundant_casts = False - -disallow_any_generics = True -disallow_untyped_defs = True -disallow_untyped_calls = True -disallow_subclassing_any = True -disallow_incomplete_defs = True -disallow_untyped_decorators = True -cache_fine_grained = True - -# By default, mypy reports an error if you assign a value to the result -# of a function call that doesn't return anything. We do this in our test -# cases: -# ``` -# result = ... -# assert result is None -# ``` -# Changing this codegen to make mypy happy would increase complexity -# and would not be worth it. -disable_error_code = func-returns-value,overload-cannot-match - -# https://github.com/python/mypy/issues/12162 -[mypy.overrides] -module = "black.files.*" -ignore_errors = true -ignore_missing_imports = true diff --git a/pyproject.toml b/pyproject.toml index f731ab32..2acfd24c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -157,6 +157,58 @@ reportOverlappingOverload = false reportImportCycles = false reportPrivateUsage = false +[tool.mypy] +pretty = true +show_error_codes = true + +# Exclude _files.py because mypy isn't smart enough to apply +# the correct type narrowing and as this is an internal module +# it's fine to just use Pyright. +# +# We also exclude our `tests` as mypy doesn't always infer +# types correctly and Pyright will still catch any type errors. +exclude = ['src/kernel/_files.py', '_dev/.*.py', 'tests/.*'] + +strict_equality = true +implicit_reexport = true +check_untyped_defs = true +no_implicit_optional = true + +warn_return_any = true +warn_unreachable = true +warn_unused_configs = true + +# Turn these options off as it could cause conflicts +# with the Pyright options. +warn_unused_ignores = false +warn_redundant_casts = false + +disallow_any_generics = true +disallow_untyped_defs = true +disallow_untyped_calls = true +disallow_subclassing_any = true +disallow_incomplete_defs = true +disallow_untyped_decorators = true +cache_fine_grained = true + +# By default, mypy reports an error if you assign a value to the result +# of a function call that doesn't return anything. We do this in our test +# cases: +# ``` +# result = ... +# assert result is None +# ``` +# Changing this codegen to make mypy happy would increase complexity +# and would not be worth it. +disable_error_code = "func-returns-value,overload-cannot-match" + +# https://github.com/python/mypy/issues/12162 +[[tool.mypy.overrides]] +module = "black.files.*" +ignore_errors = true +ignore_missing_imports = true + + [tool.ruff] line-length = 120 output-format = "grouped" From f939081b2087bee0ec0a651cc093a7c925a063ba Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 15:16:51 +0000 Subject: [PATCH 158/448] feat(api): add pagination to the deployments endpoint --- .stats.yml | 6 +- README.md | 73 ++++++++++++++++++ api.md | 2 +- src/kernel/pagination.py | 78 ++++++++++++++++++++ src/kernel/resources/deployments.py | 53 ++++++++++--- src/kernel/types/deployment_list_params.py | 10 ++- src/kernel/types/deployment_list_response.py | 11 +-- tests/api_resources/test_deployments.py | 45 +++++++---- 8 files changed, 239 insertions(+), 39 deletions(-) create mode 100644 src/kernel/pagination.py diff --git a/.stats.yml b/.stats.yml index 6ac19baf..8943c2f3 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 46 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-e98d46c55826cdf541a9ee0df04ce92806ac6d4d92957ae79f897270b7d85b23.yml -openapi_spec_hash: 8a1af54fc0a4417165b8a52e6354b685 -config_hash: 043ddc54629c6d8b889123770cb4769f +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-33f1feaba7bde46bfa36d2fefb5c3bc9512967945bccf78045ad3f64aafc4eb0.yml +openapi_spec_hash: 52a448889d41216d1ca30e8a57115b14 +config_hash: 1f28d5c3c063f418ebd2799df1e4e781 diff --git a/README.md b/README.md index 884d10c2..c5b5bf7c 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,79 @@ Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typ Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. +## Pagination + +List methods in the Kernel API are paginated. + +This library provides auto-paginating iterators with each list response, so you do not have to request successive pages manually: + +```python +from kernel import Kernel + +client = Kernel() + +all_deployments = [] +# Automatically fetches more pages as needed. +for deployment in client.deployments.list( + app_name="YOUR_APP", + limit=2, +): + # Do something with deployment here + all_deployments.append(deployment) +print(all_deployments) +``` + +Or, asynchronously: + +```python +import asyncio +from kernel import AsyncKernel + +client = AsyncKernel() + + +async def main() -> None: + all_deployments = [] + # Iterate through items across all pages, issuing requests as needed. + async for deployment in client.deployments.list( + app_name="YOUR_APP", + limit=2, + ): + all_deployments.append(deployment) + print(all_deployments) + + +asyncio.run(main()) +``` + +Alternatively, you can use the `.has_next_page()`, `.next_page_info()`, or `.get_next_page()` methods for more granular control working with pages: + +```python +first_page = await client.deployments.list( + app_name="YOUR_APP", + limit=2, +) +if first_page.has_next_page(): + print(f"will fetch next page using these details: {first_page.next_page_info()}") + next_page = await first_page.get_next_page() + print(f"number of items we just fetched: {len(next_page.items)}") + +# Remove `await` for non-async usage. +``` + +Or just work directly with the returned data: + +```python +first_page = await client.deployments.list( + app_name="YOUR_APP", + limit=2, +) +for deployment in first_page.items: + print(deployment.id) + +# Remove `await` for non-async usage. +``` + ## Nested params Nested parameters are dictionaries, typed using `TypedDict`, for example: diff --git a/api.md b/api.md index 78295850..56d852bc 100644 --- a/api.md +++ b/api.md @@ -22,7 +22,7 @@ Methods: - client.deployments.create(\*\*params) -> DeploymentCreateResponse - client.deployments.retrieve(id) -> DeploymentRetrieveResponse -- client.deployments.list(\*\*params) -> DeploymentListResponse +- client.deployments.list(\*\*params) -> SyncOffsetPagination[DeploymentListResponse] - client.deployments.follow(id, \*\*params) -> DeploymentFollowResponse # Apps diff --git a/src/kernel/pagination.py b/src/kernel/pagination.py new file mode 100644 index 00000000..2002d5eb --- /dev/null +++ b/src/kernel/pagination.py @@ -0,0 +1,78 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Any, List, Type, Generic, Mapping, TypeVar, Optional, cast +from typing_extensions import override + +from httpx import Response + +from ._utils import is_mapping +from ._models import BaseModel +from ._base_client import BasePage, PageInfo, BaseSyncPage, BaseAsyncPage + +__all__ = ["SyncOffsetPagination", "AsyncOffsetPagination"] + +_BaseModelT = TypeVar("_BaseModelT", bound=BaseModel) + +_T = TypeVar("_T") + + +class SyncOffsetPagination(BaseSyncPage[_T], BasePage[_T], Generic[_T]): + items: List[_T] + + @override + def _get_page_items(self) -> List[_T]: + items = self.items + if not items: + return [] + return items + + @override + def next_page_info(self) -> Optional[PageInfo]: + offset = self._options.params.get("offset") or 0 + if not isinstance(offset, int): + raise ValueError(f'Expected "offset" param to be an integer but got {offset}') + + length = len(self._get_page_items()) + current_count = offset + length + + return PageInfo(params={"offset": current_count}) + + @classmethod + def build(cls: Type[_BaseModelT], *, response: Response, data: object) -> _BaseModelT: # noqa: ARG003 + return cls.construct( + None, + **{ + **(cast(Mapping[str, Any], data) if is_mapping(data) else {"items": data}), + }, + ) + + +class AsyncOffsetPagination(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): + items: List[_T] + + @override + def _get_page_items(self) -> List[_T]: + items = self.items + if not items: + return [] + return items + + @override + def next_page_info(self) -> Optional[PageInfo]: + offset = self._options.params.get("offset") or 0 + if not isinstance(offset, int): + raise ValueError(f'Expected "offset" param to be an integer but got {offset}') + + length = len(self._get_page_items()) + current_count = offset + length + + return PageInfo(params={"offset": current_count}) + + @classmethod + def build(cls: Type[_BaseModelT], *, response: Response, data: object) -> _BaseModelT: # noqa: ARG003 + return cls.construct( + None, + **{ + **(cast(Mapping[str, Any], data) if is_mapping(data) else {"items": data}), + }, + ) diff --git a/src/kernel/resources/deployments.py b/src/kernel/resources/deployments.py index d54c4ec2..a288798e 100644 --- a/src/kernel/resources/deployments.py +++ b/src/kernel/resources/deployments.py @@ -19,7 +19,8 @@ async_to_streamed_response_wrapper, ) from .._streaming import Stream, AsyncStream -from .._base_client import make_request_options +from ..pagination import SyncOffsetPagination, AsyncOffsetPagination +from .._base_client import AsyncPaginator, make_request_options from ..types.deployment_list_response import DeploymentListResponse from ..types.deployment_create_response import DeploymentCreateResponse from ..types.deployment_follow_response import DeploymentFollowResponse @@ -150,14 +151,16 @@ def retrieve( def list( self, *, - app_name: str | NotGiven = NOT_GIVEN, + app_name: str, + limit: int | NotGiven = NOT_GIVEN, + offset: 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, - ) -> DeploymentListResponse: + ) -> SyncOffsetPagination[DeploymentListResponse]: """List deployments. Optionally filter by application name. @@ -165,6 +168,10 @@ def list( Args: app_name: Filter results by application name. + limit: Limit the number of deployments to return. + + offset: Offset the number of deployments to return. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -173,16 +180,24 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ - return self._get( + return self._get_api_list( "/deployments", + page=SyncOffsetPagination[DeploymentListResponse], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout, - query=maybe_transform({"app_name": app_name}, deployment_list_params.DeploymentListParams), + query=maybe_transform( + { + "app_name": app_name, + "limit": limit, + "offset": offset, + }, + deployment_list_params.DeploymentListParams, + ), ), - cast_to=DeploymentListResponse, + model=DeploymentListResponse, ) def follow( @@ -352,17 +367,19 @@ async def retrieve( cast_to=DeploymentRetrieveResponse, ) - async def list( + def list( self, *, - app_name: str | NotGiven = NOT_GIVEN, + app_name: str, + limit: int | NotGiven = NOT_GIVEN, + offset: 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, - ) -> DeploymentListResponse: + ) -> AsyncPaginator[DeploymentListResponse, AsyncOffsetPagination[DeploymentListResponse]]: """List deployments. Optionally filter by application name. @@ -370,6 +387,10 @@ async def list( Args: app_name: Filter results by application name. + limit: Limit the number of deployments to return. + + offset: Offset the number of deployments to return. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -378,16 +399,24 @@ async def list( timeout: Override the client-level default timeout for this request, in seconds """ - return await self._get( + return self._get_api_list( "/deployments", + page=AsyncOffsetPagination[DeploymentListResponse], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout, - query=await async_maybe_transform({"app_name": app_name}, deployment_list_params.DeploymentListParams), + query=maybe_transform( + { + "app_name": app_name, + "limit": limit, + "offset": offset, + }, + deployment_list_params.DeploymentListParams, + ), ), - cast_to=DeploymentListResponse, + model=DeploymentListResponse, ) async def follow( diff --git a/src/kernel/types/deployment_list_params.py b/src/kernel/types/deployment_list_params.py index 05704a19..b39b446a 100644 --- a/src/kernel/types/deployment_list_params.py +++ b/src/kernel/types/deployment_list_params.py @@ -2,11 +2,17 @@ from __future__ import annotations -from typing_extensions import TypedDict +from typing_extensions import Required, TypedDict __all__ = ["DeploymentListParams"] class DeploymentListParams(TypedDict, total=False): - app_name: str + app_name: Required[str] """Filter results by application name.""" + + limit: int + """Limit the number of deployments to return.""" + + offset: int + """Offset the number of deployments to return.""" diff --git a/src/kernel/types/deployment_list_response.py b/src/kernel/types/deployment_list_response.py index ba7759da..d22b0076 100644 --- a/src/kernel/types/deployment_list_response.py +++ b/src/kernel/types/deployment_list_response.py @@ -1,15 +1,15 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Dict, List, Optional +from typing import Dict, Optional from datetime import datetime -from typing_extensions import Literal, TypeAlias +from typing_extensions import Literal from .._models import BaseModel -__all__ = ["DeploymentListResponse", "DeploymentListResponseItem"] +__all__ = ["DeploymentListResponse"] -class DeploymentListResponseItem(BaseModel): +class DeploymentListResponse(BaseModel): id: str """Unique identifier for the deployment""" @@ -33,6 +33,3 @@ class DeploymentListResponseItem(BaseModel): updated_at: Optional[datetime] = None """Timestamp when the deployment was last updated""" - - -DeploymentListResponse: TypeAlias = List[DeploymentListResponseItem] diff --git a/tests/api_resources/test_deployments.py b/tests/api_resources/test_deployments.py index c177978b..97a90a8a 100644 --- a/tests/api_resources/test_deployments.py +++ b/tests/api_resources/test_deployments.py @@ -14,6 +14,7 @@ DeploymentCreateResponse, DeploymentRetrieveResponse, ) +from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -116,36 +117,44 @@ def test_path_params_retrieve(self, client: Kernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_list(self, client: Kernel) -> None: - deployment = client.deployments.list() - assert_matches_type(DeploymentListResponse, deployment, path=["response"]) + deployment = client.deployments.list( + app_name="app_name", + ) + assert_matches_type(SyncOffsetPagination[DeploymentListResponse], deployment, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_list_with_all_params(self, client: Kernel) -> None: deployment = client.deployments.list( app_name="app_name", + limit=1, + offset=0, ) - assert_matches_type(DeploymentListResponse, deployment, path=["response"]) + assert_matches_type(SyncOffsetPagination[DeploymentListResponse], deployment, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_list(self, client: Kernel) -> None: - response = client.deployments.with_raw_response.list() + response = client.deployments.with_raw_response.list( + app_name="app_name", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" deployment = response.parse() - assert_matches_type(DeploymentListResponse, deployment, path=["response"]) + assert_matches_type(SyncOffsetPagination[DeploymentListResponse], deployment, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_list(self, client: Kernel) -> None: - with client.deployments.with_streaming_response.list() as response: + with client.deployments.with_streaming_response.list( + app_name="app_name", + ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" deployment = response.parse() - assert_matches_type(DeploymentListResponse, deployment, path=["response"]) + assert_matches_type(SyncOffsetPagination[DeploymentListResponse], deployment, path=["response"]) assert cast(Any, response.is_closed) is True @@ -300,36 +309,44 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_list(self, async_client: AsyncKernel) -> None: - deployment = await async_client.deployments.list() - assert_matches_type(DeploymentListResponse, deployment, path=["response"]) + deployment = await async_client.deployments.list( + app_name="app_name", + ) + assert_matches_type(AsyncOffsetPagination[DeploymentListResponse], deployment, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: deployment = await async_client.deployments.list( app_name="app_name", + limit=1, + offset=0, ) - assert_matches_type(DeploymentListResponse, deployment, path=["response"]) + assert_matches_type(AsyncOffsetPagination[DeploymentListResponse], deployment, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_list(self, async_client: AsyncKernel) -> None: - response = await async_client.deployments.with_raw_response.list() + response = await async_client.deployments.with_raw_response.list( + app_name="app_name", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" deployment = await response.parse() - assert_matches_type(DeploymentListResponse, deployment, path=["response"]) + assert_matches_type(AsyncOffsetPagination[DeploymentListResponse], deployment, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: - async with async_client.deployments.with_streaming_response.list() as response: + async with async_client.deployments.with_streaming_response.list( + app_name="app_name", + ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" deployment = await response.parse() - assert_matches_type(DeploymentListResponse, deployment, path=["response"]) + assert_matches_type(AsyncOffsetPagination[DeploymentListResponse], deployment, path=["response"]) assert cast(Any, response.is_closed) is True From b865f8f2475a7049d67d07620ede0d9064060783 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 15:19:41 +0000 Subject: [PATCH 159/448] feat(api): update API spec with pagination headers --- .stats.yml | 4 ++-- src/kernel/resources/deployments.py | 4 ++-- src/kernel/types/deployment_list_params.py | 4 ++-- tests/api_resources/test_deployments.py | 24 ++++++---------------- 4 files changed, 12 insertions(+), 24 deletions(-) diff --git a/.stats.yml b/.stats.yml index 8943c2f3..9635bb22 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 46 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-33f1feaba7bde46bfa36d2fefb5c3bc9512967945bccf78045ad3f64aafc4eb0.yml -openapi_spec_hash: 52a448889d41216d1ca30e8a57115b14 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-cb38560915edce03abce2ae3ef5bc745489dbe9b6f80c2b4ff42edf8c2ff276d.yml +openapi_spec_hash: a869194d6c864ba28d79ec0105439c3e config_hash: 1f28d5c3c063f418ebd2799df1e4e781 diff --git a/src/kernel/resources/deployments.py b/src/kernel/resources/deployments.py index a288798e..5c7715de 100644 --- a/src/kernel/resources/deployments.py +++ b/src/kernel/resources/deployments.py @@ -151,7 +151,7 @@ def retrieve( def list( self, *, - app_name: str, + app_name: str | NotGiven = NOT_GIVEN, limit: int | NotGiven = NOT_GIVEN, offset: int | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -370,7 +370,7 @@ async def retrieve( def list( self, *, - app_name: str, + app_name: str | NotGiven = NOT_GIVEN, limit: int | NotGiven = NOT_GIVEN, offset: int | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. diff --git a/src/kernel/types/deployment_list_params.py b/src/kernel/types/deployment_list_params.py index b39b446a..54124da5 100644 --- a/src/kernel/types/deployment_list_params.py +++ b/src/kernel/types/deployment_list_params.py @@ -2,13 +2,13 @@ from __future__ import annotations -from typing_extensions import Required, TypedDict +from typing_extensions import TypedDict __all__ = ["DeploymentListParams"] class DeploymentListParams(TypedDict, total=False): - app_name: Required[str] + app_name: str """Filter results by application name.""" limit: int diff --git a/tests/api_resources/test_deployments.py b/tests/api_resources/test_deployments.py index 97a90a8a..fc5d2991 100644 --- a/tests/api_resources/test_deployments.py +++ b/tests/api_resources/test_deployments.py @@ -117,9 +117,7 @@ def test_path_params_retrieve(self, client: Kernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_list(self, client: Kernel) -> None: - deployment = client.deployments.list( - app_name="app_name", - ) + deployment = client.deployments.list() assert_matches_type(SyncOffsetPagination[DeploymentListResponse], deployment, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @@ -135,9 +133,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_list(self, client: Kernel) -> None: - response = client.deployments.with_raw_response.list( - app_name="app_name", - ) + response = client.deployments.with_raw_response.list() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -147,9 +143,7 @@ def test_raw_response_list(self, client: Kernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_list(self, client: Kernel) -> None: - with client.deployments.with_streaming_response.list( - app_name="app_name", - ) as response: + with client.deployments.with_streaming_response.list() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -309,9 +303,7 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_list(self, async_client: AsyncKernel) -> None: - deployment = await async_client.deployments.list( - app_name="app_name", - ) + deployment = await async_client.deployments.list() assert_matches_type(AsyncOffsetPagination[DeploymentListResponse], deployment, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @@ -327,9 +319,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_list(self, async_client: AsyncKernel) -> None: - response = await async_client.deployments.with_raw_response.list( - app_name="app_name", - ) + response = await async_client.deployments.with_raw_response.list() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -339,9 +329,7 @@ async def test_raw_response_list(self, async_client: AsyncKernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: - async with async_client.deployments.with_streaming_response.list( - app_name="app_name", - ) as response: + async with async_client.deployments.with_streaming_response.list() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" From 6e746bb0c71e346887001d4dacb7eda7fc1bb710 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 18:00:37 +0000 Subject: [PATCH 160/448] feat(api): pagination properties added to response (has_more, next_offset) --- .stats.yml | 2 +- README.md | 4 ++++ src/kernel/pagination.py | 42 +++++++++++++++++++++++++++++++--------- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/.stats.yml b/.stats.yml index 9635bb22..7fb3d31e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 46 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-cb38560915edce03abce2ae3ef5bc745489dbe9b6f80c2b4ff42edf8c2ff276d.yml openapi_spec_hash: a869194d6c864ba28d79ec0105439c3e -config_hash: 1f28d5c3c063f418ebd2799df1e4e781 +config_hash: ed56f95781ec9b2e73c97e1a66606071 diff --git a/README.md b/README.md index c5b5bf7c..beec3f01 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,10 @@ first_page = await client.deployments.list( app_name="YOUR_APP", limit=2, ) + +print( + f"the current start offset for this page: {first_page.next_offset}" +) # => "the current start offset for this page: 1" for deployment in first_page.items: print(deployment.id) diff --git a/src/kernel/pagination.py b/src/kernel/pagination.py index 2002d5eb..cdf83c2f 100644 --- a/src/kernel/pagination.py +++ b/src/kernel/pagination.py @@ -5,7 +5,7 @@ from httpx import Response -from ._utils import is_mapping +from ._utils import is_mapping, maybe_coerce_boolean, maybe_coerce_integer from ._models import BaseModel from ._base_client import BasePage, PageInfo, BaseSyncPage, BaseAsyncPage @@ -18,6 +18,8 @@ class SyncOffsetPagination(BaseSyncPage[_T], BasePage[_T], Generic[_T]): items: List[_T] + has_more: Optional[bool] = None + next_offset: Optional[int] = None @override def _get_page_items(self) -> List[_T]: @@ -26,14 +28,22 @@ def _get_page_items(self) -> List[_T]: return [] return items + @override + def has_next_page(self) -> bool: + has_more = self.has_more + if has_more is not None and has_more is False: + return False + + return super().has_next_page() + @override def next_page_info(self) -> Optional[PageInfo]: - offset = self._options.params.get("offset") or 0 - if not isinstance(offset, int): - raise ValueError(f'Expected "offset" param to be an integer but got {offset}') + next_offset = self.next_offset + if next_offset is None: + return None # type: ignore[unreachable] length = len(self._get_page_items()) - current_count = offset + length + current_count = next_offset + length return PageInfo(params={"offset": current_count}) @@ -43,12 +53,16 @@ def build(cls: Type[_BaseModelT], *, response: Response, data: object) -> _BaseM None, **{ **(cast(Mapping[str, Any], data) if is_mapping(data) else {"items": data}), + "has_more": maybe_coerce_boolean(response.headers.get("X-Has-More")), + "next_offset": maybe_coerce_integer(response.headers.get("X-Next-Offset")), }, ) class AsyncOffsetPagination(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): items: List[_T] + has_more: Optional[bool] = None + next_offset: Optional[int] = None @override def _get_page_items(self) -> List[_T]: @@ -57,14 +71,22 @@ def _get_page_items(self) -> List[_T]: return [] return items + @override + def has_next_page(self) -> bool: + has_more = self.has_more + if has_more is not None and has_more is False: + return False + + return super().has_next_page() + @override def next_page_info(self) -> Optional[PageInfo]: - offset = self._options.params.get("offset") or 0 - if not isinstance(offset, int): - raise ValueError(f'Expected "offset" param to be an integer but got {offset}') + next_offset = self.next_offset + if next_offset is None: + return None # type: ignore[unreachable] length = len(self._get_page_items()) - current_count = offset + length + current_count = next_offset + length return PageInfo(params={"offset": current_count}) @@ -74,5 +96,7 @@ def build(cls: Type[_BaseModelT], *, response: Response, data: object) -> _BaseM None, **{ **(cast(Mapping[str, Any], data) if is_mapping(data) else {"items": data}), + "has_more": maybe_coerce_boolean(response.headers.get("X-Has-More")), + "next_offset": maybe_coerce_integer(response.headers.get("X-Next-Offset")), }, ) From 62bc0f8170699941d07c9ed032e4532b9a33f92a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 6 Sep 2025 03:56:02 +0000 Subject: [PATCH 161/448] chore(tests): simplify `get_platform` test `nest_asyncio` is archived and broken on some platforms so it's not worth keeping in our test suite. --- pyproject.toml | 1 - requirements-dev.lock | 1 - tests/test_client.py | 53 +++++-------------------------------------- 3 files changed, 6 insertions(+), 49 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2acfd24c..0486b41f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,6 @@ dev-dependencies = [ "dirty-equals>=0.6.0", "importlib-metadata>=6.7.0", "rich>=13.7.1", - "nest_asyncio==1.6.0", "pytest-xdist>=3.6.1", ] diff --git a/requirements-dev.lock b/requirements-dev.lock index 55681a90..56d0accb 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -75,7 +75,6 @@ multidict==6.4.4 mypy==1.14.1 mypy-extensions==1.0.0 # via mypy -nest-asyncio==1.6.0 nodeenv==1.8.0 # via pyright nox==2023.4.22 diff --git a/tests/test_client.py b/tests/test_client.py index 86a87907..15329aee 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -6,13 +6,10 @@ import os import sys import json -import time import asyncio import inspect -import subprocess import tracemalloc from typing import Any, Union, cast -from textwrap import dedent from unittest import mock from typing_extensions import Literal @@ -23,14 +20,17 @@ from kernel import Kernel, AsyncKernel, APIResponseValidationError from kernel._types import Omit +from kernel._utils import asyncify from kernel._models import BaseModel, FinalRequestOptions from kernel._exceptions import KernelError, APIStatusError, APITimeoutError, APIResponseValidationError from kernel._base_client import ( DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, BaseClient, + OtherPlatform, DefaultHttpxClient, DefaultAsyncHttpxClient, + get_platform, make_request_options, ) @@ -1641,50 +1641,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: assert response.http_request.headers.get("x-stainless-retry-count") == "42" - def test_get_platform(self) -> None: - # A previous implementation of asyncify could leave threads unterminated when - # used with nest_asyncio. - # - # Since nest_asyncio.apply() is global and cannot be un-applied, this - # test is run in a separate process to avoid affecting other tests. - test_code = dedent(""" - import asyncio - import nest_asyncio - import threading - - from kernel._utils import asyncify - from kernel._base_client import get_platform - - async def test_main() -> None: - result = await asyncify(get_platform)() - print(result) - for thread in threading.enumerate(): - print(thread.name) - - nest_asyncio.apply() - asyncio.run(test_main()) - """) - with subprocess.Popen( - [sys.executable, "-c", test_code], - text=True, - ) as process: - timeout = 10 # seconds - - start_time = time.monotonic() - while True: - return_code = process.poll() - if return_code is not None: - if return_code != 0: - raise AssertionError("calling get_platform using asyncify resulted in a non-zero exit code") - - # success - break - - if time.monotonic() - start_time > timeout: - process.kill() - raise AssertionError("calling get_platform using asyncify resulted in a hung process") - - time.sleep(0.1) + async def test_get_platform(self) -> None: + platform = await asyncify(get_platform)() + assert isinstance(platform, (str, OtherPlatform)) async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly From 7fa052bc0de33ecec304ef9690b4013c02a9701b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 15:53:17 +0000 Subject: [PATCH 162/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index f7014c35..e82003f4 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.11.0" + ".": "0.11.1" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 0486b41f..5533264c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.11.0" +version = "0.11.1" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 1caec76c..0e02001c 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.11.0" # x-release-please-version +__version__ = "0.11.1" # x-release-please-version From 262e9b82d97f511989da7636af477b612ca70d78 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 17 Sep 2025 02:47:20 +0000 Subject: [PATCH 163/448] chore(internal): update pydantic dependency --- requirements-dev.lock | 7 +++++-- requirements.lock | 7 +++++-- src/kernel/_models.py | 14 ++++++++++---- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/requirements-dev.lock b/requirements-dev.lock index 56d0accb..249acb1e 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -88,9 +88,9 @@ pluggy==1.5.0 propcache==0.3.1 # via aiohttp # via yarl -pydantic==2.10.3 +pydantic==2.11.9 # via kernel -pydantic-core==2.27.1 +pydantic-core==2.33.2 # via pydantic pygments==2.18.0 # via rich @@ -126,6 +126,9 @@ typing-extensions==4.12.2 # via pydantic # via pydantic-core # via pyright + # via typing-inspection +typing-inspection==0.4.1 + # via pydantic virtualenv==20.24.5 # via nox yarl==1.20.0 diff --git a/requirements.lock b/requirements.lock index 61c4c7ac..49cc9054 100644 --- a/requirements.lock +++ b/requirements.lock @@ -55,9 +55,9 @@ multidict==6.4.4 propcache==0.3.1 # via aiohttp # via yarl -pydantic==2.10.3 +pydantic==2.11.9 # via kernel -pydantic-core==2.27.1 +pydantic-core==2.33.2 # via pydantic sniffio==1.3.0 # via anyio @@ -68,5 +68,8 @@ typing-extensions==4.12.2 # via multidict # via pydantic # via pydantic-core + # via typing-inspection +typing-inspection==0.4.1 + # via pydantic yarl==1.20.0 # via aiohttp diff --git a/src/kernel/_models.py b/src/kernel/_models.py index 3a6017ef..6a3cd1d2 100644 --- a/src/kernel/_models.py +++ b/src/kernel/_models.py @@ -256,7 +256,7 @@ def model_dump( mode: Literal["json", "python"] | str = "python", include: IncEx | None = None, exclude: IncEx | None = None, - by_alias: bool = False, + by_alias: bool | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, @@ -264,6 +264,7 @@ def model_dump( warnings: bool | Literal["none", "warn", "error"] = True, context: dict[str, Any] | None = None, serialize_as_any: bool = False, + fallback: Callable[[Any], Any] | None = None, ) -> dict[str, Any]: """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump @@ -295,10 +296,12 @@ def model_dump( raise ValueError("context is only supported in Pydantic v2") if serialize_as_any != False: raise ValueError("serialize_as_any is only supported in Pydantic v2") + if fallback is not None: + raise ValueError("fallback is only supported in Pydantic v2") dumped = super().dict( # pyright: ignore[reportDeprecated] include=include, exclude=exclude, - by_alias=by_alias, + by_alias=by_alias if by_alias is not None else False, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none, @@ -313,13 +316,14 @@ def model_dump_json( indent: int | None = None, include: IncEx | None = None, exclude: IncEx | None = None, - by_alias: bool = False, + by_alias: bool | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, round_trip: bool = False, warnings: bool | Literal["none", "warn", "error"] = True, context: dict[str, Any] | None = None, + fallback: Callable[[Any], Any] | None = None, serialize_as_any: bool = False, ) -> str: """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump_json @@ -348,11 +352,13 @@ def model_dump_json( raise ValueError("context is only supported in Pydantic v2") if serialize_as_any != False: raise ValueError("serialize_as_any is only supported in Pydantic v2") + if fallback is not None: + raise ValueError("fallback is only supported in Pydantic v2") return super().json( # type: ignore[reportDeprecated] indent=indent, include=include, exclude=exclude, - by_alias=by_alias, + by_alias=by_alias if by_alias is not None else False, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none, From f154cd5787b54088984e7262730d2d1f562aadb0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 19 Sep 2025 02:55:46 +0000 Subject: [PATCH 164/448] chore(types): change optional parameter type from NotGiven to Omit --- src/kernel/__init__.py | 4 +- src/kernel/_base_client.py | 18 +++--- src/kernel/_client.py | 24 ++++---- src/kernel/_qs.py | 14 ++--- src/kernel/_types.py | 29 +++++---- src/kernel/_utils/_transform.py | 4 +- src/kernel/_utils/_utils.py | 8 +-- src/kernel/resources/apps.py | 14 ++--- src/kernel/resources/browsers/browsers.py | 46 +++++++------- src/kernel/resources/browsers/fs/fs.py | 66 ++++++++++---------- src/kernel/resources/browsers/fs/watch.py | 18 +++--- src/kernel/resources/browsers/logs.py | 18 +++--- src/kernel/resources/browsers/process.py | 74 +++++++++++------------ src/kernel/resources/browsers/replays.py | 26 ++++---- src/kernel/resources/deployments.py | 50 +++++++-------- src/kernel/resources/invocations.py | 34 +++++------ src/kernel/resources/profiles.py | 26 ++++---- tests/test_transform.py | 11 +++- 18 files changed, 250 insertions(+), 234 deletions(-) diff --git a/src/kernel/__init__.py b/src/kernel/__init__.py index 4ad2b380..d1fdcc02 100644 --- a/src/kernel/__init__.py +++ b/src/kernel/__init__.py @@ -3,7 +3,7 @@ import typing as _t from . import types -from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes +from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes, omit, not_given from ._utils import file_from_path from ._client import ( ENVIRONMENTS, @@ -49,7 +49,9 @@ "ProxiesTypes", "NotGiven", "NOT_GIVEN", + "not_given", "Omit", + "omit", "KernelError", "APIError", "APIStatusError", diff --git a/src/kernel/_base_client.py b/src/kernel/_base_client.py index 0d2ff453..756e21e7 100644 --- a/src/kernel/_base_client.py +++ b/src/kernel/_base_client.py @@ -42,7 +42,6 @@ from ._qs import Querystring from ._files import to_httpx_files, async_to_httpx_files from ._types import ( - NOT_GIVEN, Body, Omit, Query, @@ -57,6 +56,7 @@ RequestOptions, HttpxRequestFiles, ModelBuilderProtocol, + not_given, ) from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping from ._compat import PYDANTIC_V1, model_copy, model_dump @@ -145,9 +145,9 @@ def __init__( def __init__( self, *, - url: URL | NotGiven = NOT_GIVEN, - json: Body | NotGiven = NOT_GIVEN, - params: Query | NotGiven = NOT_GIVEN, + url: URL | NotGiven = not_given, + json: Body | NotGiven = not_given, + params: Query | NotGiven = not_given, ) -> None: self.url = url self.json = json @@ -595,7 +595,7 @@ def _maybe_override_cast_to(self, cast_to: type[ResponseT], options: FinalReques # we internally support defining a temporary header to override the # default `cast_to` type for use with `.with_raw_response` and `.with_streaming_response` # see _response.py for implementation details - override_cast_to = headers.pop(OVERRIDE_CAST_TO_HEADER, NOT_GIVEN) + override_cast_to = headers.pop(OVERRIDE_CAST_TO_HEADER, not_given) if is_given(override_cast_to): options.headers = headers return cast(Type[ResponseT], override_cast_to) @@ -825,7 +825,7 @@ def __init__( version: str, base_url: str | URL, max_retries: int = DEFAULT_MAX_RETRIES, - timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | Timeout | None | NotGiven = not_given, http_client: httpx.Client | None = None, custom_headers: Mapping[str, str] | None = None, custom_query: Mapping[str, object] | None = None, @@ -1356,7 +1356,7 @@ def __init__( base_url: str | URL, _strict_response_validation: bool, max_retries: int = DEFAULT_MAX_RETRIES, - timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | Timeout | None | NotGiven = not_given, http_client: httpx.AsyncClient | None = None, custom_headers: Mapping[str, str] | None = None, custom_query: Mapping[str, object] | None = None, @@ -1818,8 +1818,8 @@ def make_request_options( extra_query: Query | None = None, extra_body: Body | None = None, idempotency_key: str | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - post_parser: PostParser | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + post_parser: PostParser | NotGiven = not_given, ) -> RequestOptions: """Create a dict of type RequestOptions without keys of NotGiven values.""" options: RequestOptions = {} diff --git a/src/kernel/_client.py b/src/kernel/_client.py index 3b4235cd..830aeb58 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -3,7 +3,7 @@ from __future__ import annotations import os -from typing import Any, Dict, Union, Mapping, cast +from typing import Any, Dict, Mapping, cast from typing_extensions import Self, Literal, override import httpx @@ -11,13 +11,13 @@ from . import _exceptions from ._qs import Querystring from ._types import ( - NOT_GIVEN, Omit, Timeout, NotGiven, Transport, ProxiesTypes, RequestOptions, + not_given, ) from ._utils import is_given, get_async_library from ._version import __version__ @@ -67,9 +67,9 @@ def __init__( self, *, api_key: str | None = None, - environment: Literal["production", "development"] | NotGiven = NOT_GIVEN, - base_url: str | httpx.URL | None | NotGiven = NOT_GIVEN, - timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN, + environment: Literal["production", "development"] | NotGiven = not_given, + base_url: str | httpx.URL | None | NotGiven = not_given, + timeout: float | Timeout | None | NotGiven = not_given, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, default_query: Mapping[str, object] | None = None, @@ -170,9 +170,9 @@ def copy( api_key: str | None = None, environment: Literal["production", "development"] | None = None, base_url: str | httpx.URL | None = None, - timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | Timeout | None | NotGiven = not_given, http_client: httpx.Client | None = None, - max_retries: int | NotGiven = NOT_GIVEN, + max_retries: int | NotGiven = not_given, default_headers: Mapping[str, str] | None = None, set_default_headers: Mapping[str, str] | None = None, default_query: Mapping[str, object] | None = None, @@ -269,9 +269,9 @@ def __init__( self, *, api_key: str | None = None, - environment: Literal["production", "development"] | NotGiven = NOT_GIVEN, - base_url: str | httpx.URL | None | NotGiven = NOT_GIVEN, - timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN, + environment: Literal["production", "development"] | NotGiven = not_given, + base_url: str | httpx.URL | None | NotGiven = not_given, + timeout: float | Timeout | None | NotGiven = not_given, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, default_query: Mapping[str, object] | None = None, @@ -372,9 +372,9 @@ def copy( api_key: str | None = None, environment: Literal["production", "development"] | None = None, base_url: str | httpx.URL | None = None, - timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | Timeout | None | NotGiven = not_given, http_client: httpx.AsyncClient | None = None, - max_retries: int | NotGiven = NOT_GIVEN, + max_retries: int | NotGiven = not_given, default_headers: Mapping[str, str] | None = None, set_default_headers: Mapping[str, str] | None = None, default_query: Mapping[str, object] | None = None, diff --git a/src/kernel/_qs.py b/src/kernel/_qs.py index 274320ca..ada6fd3f 100644 --- a/src/kernel/_qs.py +++ b/src/kernel/_qs.py @@ -4,7 +4,7 @@ from urllib.parse import parse_qs, urlencode from typing_extensions import Literal, get_args -from ._types import NOT_GIVEN, NotGiven, NotGivenOr +from ._types import NotGiven, not_given from ._utils import flatten _T = TypeVar("_T") @@ -41,8 +41,8 @@ def stringify( self, params: Params, *, - array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, - nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, + array_format: ArrayFormat | NotGiven = not_given, + nested_format: NestedFormat | NotGiven = not_given, ) -> str: return urlencode( self.stringify_items( @@ -56,8 +56,8 @@ def stringify_items( self, params: Params, *, - array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, - nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, + array_format: ArrayFormat | NotGiven = not_given, + nested_format: NestedFormat | NotGiven = not_given, ) -> list[tuple[str, str]]: opts = Options( qs=self, @@ -143,8 +143,8 @@ def __init__( self, qs: Querystring = _qs, *, - array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, - nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, + array_format: ArrayFormat | NotGiven = not_given, + nested_format: NestedFormat | NotGiven = not_given, ) -> None: self.array_format = qs.array_format if isinstance(array_format, NotGiven) else array_format self.nested_format = qs.nested_format if isinstance(nested_format, NotGiven) else nested_format diff --git a/src/kernel/_types.py b/src/kernel/_types.py index 48bae95e..2c1d83b2 100644 --- a/src/kernel/_types.py +++ b/src/kernel/_types.py @@ -117,18 +117,21 @@ class RequestOptions(TypedDict, total=False): # Sentinel class used until PEP 0661 is accepted class NotGiven: """ - A sentinel singleton class used to distinguish omitted keyword arguments - from those passed in with the value None (which may have different behavior). + For parameters with a meaningful None value, we need to distinguish between + the user explicitly passing None, and the user not passing the parameter at + all. + + User code shouldn't need to use not_given directly. For example: ```py - def get(timeout: Union[int, NotGiven, None] = NotGiven()) -> Response: ... + def create(timeout: Timeout | None | NotGiven = not_given): ... - get(timeout=1) # 1s timeout - get(timeout=None) # No timeout - get() # Default timeout behavior, which may not be statically known at the method definition. + create(timeout=1) # 1s timeout + create(timeout=None) # No timeout + create() # Default timeout behavior ``` """ @@ -140,13 +143,14 @@ def __repr__(self) -> str: return "NOT_GIVEN" -NotGivenOr = Union[_T, NotGiven] +not_given = NotGiven() +# for backwards compatibility: NOT_GIVEN = NotGiven() class Omit: - """In certain situations you need to be able to represent a case where a default value has - to be explicitly removed and `None` is not an appropriate substitute, for example: + """ + To explicitly omit something from being sent in a request, use `omit`. ```py # as the default `Content-Type` header is `application/json` that will be sent @@ -156,8 +160,8 @@ class Omit: # to look something like: 'multipart/form-data; boundary=0d8382fcf5f8c3be01ca2e11002d2983' client.post(..., headers={"Content-Type": "multipart/form-data"}) - # instead you can remove the default `application/json` header by passing Omit - client.post(..., headers={"Content-Type": Omit()}) + # instead you can remove the default `application/json` header by passing omit + client.post(..., headers={"Content-Type": omit}) ``` """ @@ -165,6 +169,9 @@ def __bool__(self) -> Literal[False]: return False +omit = Omit() + + @runtime_checkable class ModelBuilderProtocol(Protocol): @classmethod diff --git a/src/kernel/_utils/_transform.py b/src/kernel/_utils/_transform.py index c19124f0..52075492 100644 --- a/src/kernel/_utils/_transform.py +++ b/src/kernel/_utils/_transform.py @@ -268,7 +268,7 @@ def _transform_typeddict( annotations = get_type_hints(expected_type, include_extras=True) for key, value in data.items(): if not is_given(value): - # we don't need to include `NotGiven` values here as they'll + # we don't need to include omitted values here as they'll # be stripped out before the request is sent anyway continue @@ -434,7 +434,7 @@ async def _async_transform_typeddict( annotations = get_type_hints(expected_type, include_extras=True) for key, value in data.items(): if not is_given(value): - # we don't need to include `NotGiven` values here as they'll + # we don't need to include omitted values here as they'll # be stripped out before the request is sent anyway continue diff --git a/src/kernel/_utils/_utils.py b/src/kernel/_utils/_utils.py index f0818595..50d59269 100644 --- a/src/kernel/_utils/_utils.py +++ b/src/kernel/_utils/_utils.py @@ -21,7 +21,7 @@ import sniffio -from .._types import NotGiven, FileTypes, NotGivenOr, HeadersLike +from .._types import Omit, NotGiven, FileTypes, HeadersLike _T = TypeVar("_T") _TupleT = TypeVar("_TupleT", bound=Tuple[object, ...]) @@ -63,7 +63,7 @@ def _extract_items( try: key = path[index] except IndexError: - if isinstance(obj, NotGiven): + if not is_given(obj): # no value was provided - we can safely ignore return [] @@ -126,8 +126,8 @@ def _extract_items( return [] -def is_given(obj: NotGivenOr[_T]) -> TypeGuard[_T]: - return not isinstance(obj, NotGiven) +def is_given(obj: _T | NotGiven | Omit) -> TypeGuard[_T]: + return not isinstance(obj, NotGiven) and not isinstance(obj, Omit) # Type safe methods for narrowing types with TypeVars. diff --git a/src/kernel/resources/apps.py b/src/kernel/resources/apps.py index 652235e2..28117b98 100644 --- a/src/kernel/resources/apps.py +++ b/src/kernel/resources/apps.py @@ -5,7 +5,7 @@ import httpx from ..types import app_list_params -from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -44,14 +44,14 @@ def with_streaming_response(self) -> AppsResourceWithStreamingResponse: def list( self, *, - app_name: str | NotGiven = NOT_GIVEN, - version: str | NotGiven = NOT_GIVEN, + app_name: str | Omit = omit, + version: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AppListResponse: """List applications. @@ -112,14 +112,14 @@ def with_streaming_response(self) -> AsyncAppsResourceWithStreamingResponse: async def list( self, *, - app_name: str | NotGiven = NOT_GIVEN, - version: str | NotGiven = NOT_GIVEN, + app_name: str | Omit = omit, + version: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AppListResponse: """List applications. diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 7394f21a..e871c215 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -37,7 +37,7 @@ ReplaysResourceWithStreamingResponse, AsyncReplaysResourceWithStreamingResponse, ) -from ..._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven +from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource @@ -95,18 +95,18 @@ def with_streaming_response(self) -> BrowsersResourceWithStreamingResponse: def create( self, *, - headless: bool | NotGiven = NOT_GIVEN, - invocation_id: str | NotGiven = NOT_GIVEN, - persistence: BrowserPersistenceParam | NotGiven = NOT_GIVEN, - profile: browser_create_params.Profile | NotGiven = NOT_GIVEN, - stealth: bool | NotGiven = NOT_GIVEN, - timeout_seconds: int | NotGiven = NOT_GIVEN, + headless: bool | Omit = omit, + invocation_id: str | Omit = omit, + persistence: BrowserPersistenceParam | Omit = omit, + profile: browser_create_params.Profile | Omit = omit, + stealth: bool | Omit = omit, + timeout_seconds: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> BrowserCreateResponse: """ Create a new browser session from within an action. @@ -166,7 +166,7 @@ def retrieve( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> BrowserRetrieveResponse: """ Get information about a browser session. @@ -198,7 +198,7 @@ def list( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> BrowserListResponse: """List active browser sessions""" return self._get( @@ -218,7 +218,7 @@ def delete( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ Delete a persistent browser session by its persistent_id. @@ -256,7 +256,7 @@ def delete_by_id( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ Delete a browser session by ID @@ -321,18 +321,18 @@ def with_streaming_response(self) -> AsyncBrowsersResourceWithStreamingResponse: async def create( self, *, - headless: bool | NotGiven = NOT_GIVEN, - invocation_id: str | NotGiven = NOT_GIVEN, - persistence: BrowserPersistenceParam | NotGiven = NOT_GIVEN, - profile: browser_create_params.Profile | NotGiven = NOT_GIVEN, - stealth: bool | NotGiven = NOT_GIVEN, - timeout_seconds: int | NotGiven = NOT_GIVEN, + headless: bool | Omit = omit, + invocation_id: str | Omit = omit, + persistence: BrowserPersistenceParam | Omit = omit, + profile: browser_create_params.Profile | Omit = omit, + stealth: bool | Omit = omit, + timeout_seconds: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> BrowserCreateResponse: """ Create a new browser session from within an action. @@ -392,7 +392,7 @@ async def retrieve( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> BrowserRetrieveResponse: """ Get information about a browser session. @@ -424,7 +424,7 @@ async def list( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> BrowserListResponse: """List active browser sessions""" return await self._get( @@ -444,7 +444,7 @@ async def delete( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ Delete a persistent browser session by its persistent_id. @@ -484,7 +484,7 @@ async def delete_by_id( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ Delete a browser session by ID diff --git a/src/kernel/resources/browsers/fs/fs.py b/src/kernel/resources/browsers/fs/fs.py index cb7b3010..ff0cc48a 100644 --- a/src/kernel/resources/browsers/fs/fs.py +++ b/src/kernel/resources/browsers/fs/fs.py @@ -15,7 +15,7 @@ AsyncWatchResourceWithStreamingResponse, ) from ...._files import read_file_content, async_read_file_content -from ...._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven, FileTypes, FileContent +from ...._types import Body, Omit, Query, Headers, NoneType, NotGiven, FileTypes, FileContent, omit, not_given from ...._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform from ...._compat import cached_property from ...._resource import SyncAPIResource, AsyncAPIResource @@ -83,13 +83,13 @@ def create_directory( id: str, *, path: str, - mode: str | NotGiven = NOT_GIVEN, + mode: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ Create a new directory @@ -135,7 +135,7 @@ def delete_directory( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ Delete a directory @@ -173,7 +173,7 @@ def delete_file( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ Delete a file @@ -211,7 +211,7 @@ def download_dir_zip( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> BinaryAPIResponse: """ Returns a ZIP file containing the contents of the specified directory. @@ -252,7 +252,7 @@ def file_info( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> FFileInfoResponse: """ Get information about a file or directory @@ -292,7 +292,7 @@ def list_files( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> FListFilesResponse: """ List files in a directory @@ -333,7 +333,7 @@ def move( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ Move or rename a file or directory @@ -379,7 +379,7 @@ def read_file( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> BinaryAPIResponse: """ Read file contents @@ -416,14 +416,14 @@ def set_file_permissions( *, mode: str, path: str, - group: str | NotGiven = NOT_GIVEN, - owner: str | NotGiven = NOT_GIVEN, + group: str | Omit = omit, + owner: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ Set file or directory permissions/ownership @@ -475,7 +475,7 @@ def upload( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ Allows uploading single or multiple files to the remote filesystem. @@ -519,7 +519,7 @@ def upload_zip( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ Upload a zip file and extract its contents to the specified destination path. @@ -565,13 +565,13 @@ def write_file( contents: FileContent, *, path: str, - mode: str | NotGiven = NOT_GIVEN, + mode: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ Write or create a file @@ -642,13 +642,13 @@ async def create_directory( id: str, *, path: str, - mode: str | NotGiven = NOT_GIVEN, + mode: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ Create a new directory @@ -694,7 +694,7 @@ async def delete_directory( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ Delete a directory @@ -732,7 +732,7 @@ async def delete_file( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ Delete a file @@ -770,7 +770,7 @@ async def download_dir_zip( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncBinaryAPIResponse: """ Returns a ZIP file containing the contents of the specified directory. @@ -811,7 +811,7 @@ async def file_info( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> FFileInfoResponse: """ Get information about a file or directory @@ -851,7 +851,7 @@ async def list_files( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> FListFilesResponse: """ List files in a directory @@ -892,7 +892,7 @@ async def move( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ Move or rename a file or directory @@ -938,7 +938,7 @@ async def read_file( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncBinaryAPIResponse: """ Read file contents @@ -975,14 +975,14 @@ async def set_file_permissions( *, mode: str, path: str, - group: str | NotGiven = NOT_GIVEN, - owner: str | NotGiven = NOT_GIVEN, + group: str | Omit = omit, + owner: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ Set file or directory permissions/ownership @@ -1034,7 +1034,7 @@ async def upload( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ Allows uploading single or multiple files to the remote filesystem. @@ -1078,7 +1078,7 @@ async def upload_zip( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ Upload a zip file and extract its contents to the specified destination path. @@ -1124,13 +1124,13 @@ async def write_file( contents: FileContent, *, path: str, - mode: str | NotGiven = NOT_GIVEN, + mode: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ Write or create a file diff --git a/src/kernel/resources/browsers/fs/watch.py b/src/kernel/resources/browsers/fs/watch.py index a35e0def..ad26f2ae 100644 --- a/src/kernel/resources/browsers/fs/watch.py +++ b/src/kernel/resources/browsers/fs/watch.py @@ -4,7 +4,7 @@ import httpx -from ...._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven +from ...._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given from ...._utils import maybe_transform, async_maybe_transform from ...._compat import cached_property from ...._resource import SyncAPIResource, AsyncAPIResource @@ -53,7 +53,7 @@ def events( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Stream[WatchEventsResponse]: """ Stream filesystem events for a watch @@ -87,13 +87,13 @@ def start( id: str, *, path: str, - recursive: bool | NotGiven = NOT_GIVEN, + recursive: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> WatchStartResponse: """ Watch a directory for changes @@ -138,7 +138,7 @@ def stop( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ Stop watching a directory @@ -196,7 +196,7 @@ async def events( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncStream[WatchEventsResponse]: """ Stream filesystem events for a watch @@ -230,13 +230,13 @@ async def start( id: str, *, path: str, - recursive: bool | NotGiven = NOT_GIVEN, + recursive: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> WatchStartResponse: """ Watch a directory for changes @@ -281,7 +281,7 @@ async def stop( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ Stop watching a directory diff --git a/src/kernel/resources/browsers/logs.py b/src/kernel/resources/browsers/logs.py index fbbe14a5..1fd291d4 100644 --- a/src/kernel/resources/browsers/logs.py +++ b/src/kernel/resources/browsers/logs.py @@ -6,7 +6,7 @@ import httpx -from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource @@ -49,15 +49,15 @@ def stream( id: str, *, source: Literal["path", "supervisor"], - follow: bool | NotGiven = NOT_GIVEN, - path: str | NotGiven = NOT_GIVEN, - supervisor_process: str | NotGiven = NOT_GIVEN, + follow: bool | Omit = omit, + path: str | Omit = omit, + supervisor_process: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Stream[LogEvent]: """ Stream log files on the browser instance via SSE @@ -126,15 +126,15 @@ async def stream( id: str, *, source: Literal["path", "supervisor"], - follow: bool | NotGiven = NOT_GIVEN, - path: str | NotGiven = NOT_GIVEN, - supervisor_process: str | NotGiven = NOT_GIVEN, + follow: bool | Omit = omit, + path: str | Omit = omit, + supervisor_process: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncStream[LogEvent]: """ Stream log files on the browser instance via SSE diff --git a/src/kernel/resources/browsers/process.py b/src/kernel/resources/browsers/process.py index 9fd6dd6c..2bdaeebe 100644 --- a/src/kernel/resources/browsers/process.py +++ b/src/kernel/resources/browsers/process.py @@ -7,7 +7,7 @@ import httpx -from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, SequenceNotStr +from ..._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource @@ -55,18 +55,18 @@ def exec( id: str, *, command: str, - args: SequenceNotStr[str] | NotGiven = NOT_GIVEN, - as_root: bool | NotGiven = NOT_GIVEN, - as_user: Optional[str] | NotGiven = NOT_GIVEN, - cwd: Optional[str] | NotGiven = NOT_GIVEN, - env: Dict[str, str] | NotGiven = NOT_GIVEN, - timeout_sec: Optional[int] | NotGiven = NOT_GIVEN, + args: SequenceNotStr[str] | Omit = omit, + as_root: bool | Omit = omit, + as_user: Optional[str] | Omit = omit, + cwd: Optional[str] | Omit = omit, + env: Dict[str, str] | Omit = omit, + timeout_sec: Optional[int] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProcessExecResponse: """ Execute a command synchronously @@ -127,7 +127,7 @@ def kill( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProcessKillResponse: """ Send signal to process @@ -161,18 +161,18 @@ def spawn( id: str, *, command: str, - args: SequenceNotStr[str] | NotGiven = NOT_GIVEN, - as_root: bool | NotGiven = NOT_GIVEN, - as_user: Optional[str] | NotGiven = NOT_GIVEN, - cwd: Optional[str] | NotGiven = NOT_GIVEN, - env: Dict[str, str] | NotGiven = NOT_GIVEN, - timeout_sec: Optional[int] | NotGiven = NOT_GIVEN, + args: SequenceNotStr[str] | Omit = omit, + as_root: bool | Omit = omit, + as_user: Optional[str] | Omit = omit, + cwd: Optional[str] | Omit = omit, + env: Dict[str, str] | Omit = omit, + timeout_sec: Optional[int] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProcessSpawnResponse: """ Execute a command asynchronously @@ -232,7 +232,7 @@ def status( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProcessStatusResponse: """ Get process status @@ -269,7 +269,7 @@ def stdin( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProcessStdinResponse: """ Write to process stdin @@ -308,7 +308,7 @@ def stdout_stream( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Stream[ProcessStdoutStreamResponse]: """ Stream process stdout via SSE @@ -363,18 +363,18 @@ async def exec( id: str, *, command: str, - args: SequenceNotStr[str] | NotGiven = NOT_GIVEN, - as_root: bool | NotGiven = NOT_GIVEN, - as_user: Optional[str] | NotGiven = NOT_GIVEN, - cwd: Optional[str] | NotGiven = NOT_GIVEN, - env: Dict[str, str] | NotGiven = NOT_GIVEN, - timeout_sec: Optional[int] | NotGiven = NOT_GIVEN, + args: SequenceNotStr[str] | Omit = omit, + as_root: bool | Omit = omit, + as_user: Optional[str] | Omit = omit, + cwd: Optional[str] | Omit = omit, + env: Dict[str, str] | Omit = omit, + timeout_sec: Optional[int] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProcessExecResponse: """ Execute a command synchronously @@ -435,7 +435,7 @@ async def kill( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProcessKillResponse: """ Send signal to process @@ -469,18 +469,18 @@ async def spawn( id: str, *, command: str, - args: SequenceNotStr[str] | NotGiven = NOT_GIVEN, - as_root: bool | NotGiven = NOT_GIVEN, - as_user: Optional[str] | NotGiven = NOT_GIVEN, - cwd: Optional[str] | NotGiven = NOT_GIVEN, - env: Dict[str, str] | NotGiven = NOT_GIVEN, - timeout_sec: Optional[int] | NotGiven = NOT_GIVEN, + args: SequenceNotStr[str] | Omit = omit, + as_root: bool | Omit = omit, + as_user: Optional[str] | Omit = omit, + cwd: Optional[str] | Omit = omit, + env: Dict[str, str] | Omit = omit, + timeout_sec: Optional[int] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProcessSpawnResponse: """ Execute a command asynchronously @@ -540,7 +540,7 @@ async def status( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProcessStatusResponse: """ Get process status @@ -577,7 +577,7 @@ async def stdin( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProcessStdinResponse: """ Write to process stdin @@ -616,7 +616,7 @@ async def stdout_stream( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncStream[ProcessStdoutStreamResponse]: """ Stream process stdout via SSE diff --git a/src/kernel/resources/browsers/replays.py b/src/kernel/resources/browsers/replays.py index b801e8f0..9f15554a 100644 --- a/src/kernel/resources/browsers/replays.py +++ b/src/kernel/resources/browsers/replays.py @@ -4,7 +4,7 @@ import httpx -from ..._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven +from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource @@ -59,7 +59,7 @@ def list( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ReplayListResponse: """ List all replays for the specified browser session. @@ -93,7 +93,7 @@ def download( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> BinaryAPIResponse: """ Download or stream the specified replay recording. @@ -124,14 +124,14 @@ def start( self, id: str, *, - framerate: int | NotGiven = NOT_GIVEN, - max_duration_in_seconds: int | NotGiven = NOT_GIVEN, + framerate: int | Omit = omit, + max_duration_in_seconds: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ReplayStartResponse: """ Start recording the browser session and return a replay ID. @@ -176,7 +176,7 @@ def stop( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ Stop the specified replay recording and persist the video. @@ -233,7 +233,7 @@ async def list( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ReplayListResponse: """ List all replays for the specified browser session. @@ -267,7 +267,7 @@ async def download( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncBinaryAPIResponse: """ Download or stream the specified replay recording. @@ -298,14 +298,14 @@ async def start( self, id: str, *, - framerate: int | NotGiven = NOT_GIVEN, - max_duration_in_seconds: int | NotGiven = NOT_GIVEN, + framerate: int | Omit = omit, + max_duration_in_seconds: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ReplayStartResponse: """ Start recording the browser session and return a replay ID. @@ -350,7 +350,7 @@ async def stop( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ Stop the specified replay recording and persist the video. diff --git a/src/kernel/resources/deployments.py b/src/kernel/resources/deployments.py index 5c7715de..15812440 100644 --- a/src/kernel/resources/deployments.py +++ b/src/kernel/resources/deployments.py @@ -8,7 +8,7 @@ import httpx from ..types import deployment_list_params, deployment_create_params, deployment_follow_params -from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes +from .._types import Body, Omit, Query, Headers, NotGiven, FileTypes, omit, not_given from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -54,16 +54,16 @@ def create( *, entrypoint_rel_path: str, file: FileTypes, - env_vars: Dict[str, str] | NotGiven = NOT_GIVEN, - force: bool | NotGiven = NOT_GIVEN, - region: Literal["aws.us-east-1a"] | NotGiven = NOT_GIVEN, - version: str | NotGiven = NOT_GIVEN, + env_vars: Dict[str, str] | Omit = omit, + force: bool | Omit = omit, + region: Literal["aws.us-east-1a"] | Omit = omit, + version: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> DeploymentCreateResponse: """ Create a new deployment. @@ -124,7 +124,7 @@ def retrieve( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> DeploymentRetrieveResponse: """ Get information about a deployment's status. @@ -151,15 +151,15 @@ def retrieve( def list( self, *, - app_name: str | NotGiven = NOT_GIVEN, - limit: int | NotGiven = NOT_GIVEN, - offset: int | NotGiven = NOT_GIVEN, + app_name: str | Omit = omit, + limit: int | Omit = omit, + offset: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SyncOffsetPagination[DeploymentListResponse]: """List deployments. @@ -204,13 +204,13 @@ def follow( self, id: str, *, - since: str | NotGiven = NOT_GIVEN, + since: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Stream[DeploymentFollowResponse]: """ Establishes a Server-Sent Events (SSE) stream that delivers real-time logs and @@ -273,16 +273,16 @@ async def create( *, entrypoint_rel_path: str, file: FileTypes, - env_vars: Dict[str, str] | NotGiven = NOT_GIVEN, - force: bool | NotGiven = NOT_GIVEN, - region: Literal["aws.us-east-1a"] | NotGiven = NOT_GIVEN, - version: str | NotGiven = NOT_GIVEN, + env_vars: Dict[str, str] | Omit = omit, + force: bool | Omit = omit, + region: Literal["aws.us-east-1a"] | Omit = omit, + version: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> DeploymentCreateResponse: """ Create a new deployment. @@ -343,7 +343,7 @@ async def retrieve( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> DeploymentRetrieveResponse: """ Get information about a deployment's status. @@ -370,15 +370,15 @@ async def retrieve( def list( self, *, - app_name: str | NotGiven = NOT_GIVEN, - limit: int | NotGiven = NOT_GIVEN, - offset: int | NotGiven = NOT_GIVEN, + app_name: str | Omit = omit, + limit: int | Omit = omit, + offset: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncPaginator[DeploymentListResponse, AsyncOffsetPagination[DeploymentListResponse]]: """List deployments. @@ -423,13 +423,13 @@ async def follow( self, id: str, *, - since: str | NotGiven = NOT_GIVEN, + since: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncStream[DeploymentFollowResponse]: """ Establishes a Server-Sent Events (SSE) stream that delivers real-time logs and diff --git a/src/kernel/resources/invocations.py b/src/kernel/resources/invocations.py index 3de46d0b..2a3848a0 100644 --- a/src/kernel/resources/invocations.py +++ b/src/kernel/resources/invocations.py @@ -8,7 +8,7 @@ import httpx from ..types import invocation_create_params, invocation_update_params -from .._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven +from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -54,14 +54,14 @@ def create( action_name: str, app_name: str, version: str, - async_: bool | NotGiven = NOT_GIVEN, - payload: str | NotGiven = NOT_GIVEN, + async_: bool | Omit = omit, + payload: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> InvocationCreateResponse: """ Invoke an action. @@ -113,7 +113,7 @@ def retrieve( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> InvocationRetrieveResponse: """ Get details about an invocation's status and output. @@ -142,13 +142,13 @@ def update( id: str, *, status: Literal["succeeded", "failed"], - output: str | NotGiven = NOT_GIVEN, + output: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> InvocationUpdateResponse: """ Update an invocation's status or output. @@ -192,7 +192,7 @@ def delete_browsers( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ Delete all browser sessions created within the specified invocation. @@ -226,7 +226,7 @@ def follow( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Stream[InvocationFollowResponse]: """ Establishes a Server-Sent Events (SSE) stream that delivers real-time logs and @@ -284,14 +284,14 @@ async def create( action_name: str, app_name: str, version: str, - async_: bool | NotGiven = NOT_GIVEN, - payload: str | NotGiven = NOT_GIVEN, + async_: bool | Omit = omit, + payload: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> InvocationCreateResponse: """ Invoke an action. @@ -343,7 +343,7 @@ async def retrieve( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> InvocationRetrieveResponse: """ Get details about an invocation's status and output. @@ -372,13 +372,13 @@ async def update( id: str, *, status: Literal["succeeded", "failed"], - output: str | NotGiven = NOT_GIVEN, + output: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> InvocationUpdateResponse: """ Update an invocation's status or output. @@ -422,7 +422,7 @@ async def delete_browsers( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ Delete all browser sessions created within the specified invocation. @@ -456,7 +456,7 @@ async def follow( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncStream[InvocationFollowResponse]: """ Establishes a Server-Sent Events (SSE) stream that delivers real-time logs and diff --git a/src/kernel/resources/profiles.py b/src/kernel/resources/profiles.py index 0818cdc9..8d51da38 100644 --- a/src/kernel/resources/profiles.py +++ b/src/kernel/resources/profiles.py @@ -5,7 +5,7 @@ import httpx from ..types import profile_create_params -from .._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven +from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -53,13 +53,13 @@ def with_streaming_response(self) -> ProfilesResourceWithStreamingResponse: def create( self, *, - name: str | NotGiven = NOT_GIVEN, + name: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Profile: """ Create a browser profile that can be used to load state into future browser @@ -94,7 +94,7 @@ def retrieve( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Profile: """ Retrieve details for a single profile by its ID or name. @@ -126,7 +126,7 @@ def list( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProfileListResponse: """List profiles with optional filtering and pagination.""" return self._get( @@ -146,7 +146,7 @@ def delete( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ Delete a profile by its ID or by its name. @@ -180,7 +180,7 @@ def download( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> BinaryAPIResponse: """Download the profile. @@ -231,13 +231,13 @@ def with_streaming_response(self) -> AsyncProfilesResourceWithStreamingResponse: async def create( self, *, - name: str | NotGiven = NOT_GIVEN, + name: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Profile: """ Create a browser profile that can be used to load state into future browser @@ -272,7 +272,7 @@ async def retrieve( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Profile: """ Retrieve details for a single profile by its ID or name. @@ -304,7 +304,7 @@ async def list( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProfileListResponse: """List profiles with optional filtering and pagination.""" return await self._get( @@ -324,7 +324,7 @@ async def delete( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ Delete a profile by its ID or by its name. @@ -358,7 +358,7 @@ async def download( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncBinaryAPIResponse: """Download the profile. diff --git a/tests/test_transform.py b/tests/test_transform.py index 5f7ab31c..68ca9b26 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -8,7 +8,7 @@ import pytest -from kernel._types import NOT_GIVEN, Base64FileInput +from kernel._types import Base64FileInput, omit, not_given from kernel._utils import ( PropertyInfo, transform as _transform, @@ -450,4 +450,11 @@ async def test_transform_skipping(use_async: bool) -> None: @pytest.mark.asyncio async def test_strips_notgiven(use_async: bool) -> None: assert await transform({"foo_bar": "bar"}, Foo1, use_async) == {"fooBar": "bar"} - assert await transform({"foo_bar": NOT_GIVEN}, Foo1, use_async) == {} + assert await transform({"foo_bar": not_given}, Foo1, use_async) == {} + + +@parametrize +@pytest.mark.asyncio +async def test_strips_omit(use_async: bool) -> None: + assert await transform({"foo_bar": "bar"}, Foo1, use_async) == {"fooBar": "bar"} + assert await transform({"foo_bar": omit}, Foo1, use_async) == {} From cf31c707a14dca23a748f008b4ee9536e22f73f2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 20 Sep 2025 03:02:13 +0000 Subject: [PATCH 165/448] chore: do not install brew dependencies in ./scripts/bootstrap by default --- scripts/bootstrap | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/scripts/bootstrap b/scripts/bootstrap index e84fe62c..b430fee3 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -4,10 +4,18 @@ set -e cd "$(dirname "$0")/.." -if ! command -v rye >/dev/null 2>&1 && [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ]; then +if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then brew bundle check >/dev/null 2>&1 || { - echo "==> Installing Homebrew dependencies…" - brew bundle + echo -n "==> Install Homebrew dependencies? (y/N): " + read -r response + case "$response" in + [yY][eE][sS]|[yY]) + brew bundle + ;; + *) + ;; + esac + echo } fi From 5a3ffc5a493c29ed9dfbeb9b9703a13d938439e5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 23 Sep 2025 02:34:39 +0000 Subject: [PATCH 166/448] chore: improve example values --- tests/api_resources/test_browsers.py | 24 ++++++++++++------------ tests/api_resources/test_invocations.py | 12 ++++++------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index be4344f4..f463cf65 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -70,7 +70,7 @@ def test_streaming_response_create(self, client: Kernel) -> None: @parametrize def test_method_retrieve(self, client: Kernel) -> None: browser = client.browsers.retrieve( - "id", + "htzv5orfit78e1m2biiifpbv", ) assert_matches_type(BrowserRetrieveResponse, browser, path=["response"]) @@ -78,7 +78,7 @@ def test_method_retrieve(self, client: Kernel) -> None: @parametrize def test_raw_response_retrieve(self, client: Kernel) -> None: response = client.browsers.with_raw_response.retrieve( - "id", + "htzv5orfit78e1m2biiifpbv", ) assert response.is_closed is True @@ -90,7 +90,7 @@ def test_raw_response_retrieve(self, client: Kernel) -> None: @parametrize def test_streaming_response_retrieve(self, client: Kernel) -> None: with client.browsers.with_streaming_response.retrieve( - "id", + "htzv5orfit78e1m2biiifpbv", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -174,7 +174,7 @@ def test_streaming_response_delete(self, client: Kernel) -> None: @parametrize def test_method_delete_by_id(self, client: Kernel) -> None: browser = client.browsers.delete_by_id( - "id", + "htzv5orfit78e1m2biiifpbv", ) assert browser is None @@ -182,7 +182,7 @@ def test_method_delete_by_id(self, client: Kernel) -> None: @parametrize def test_raw_response_delete_by_id(self, client: Kernel) -> None: response = client.browsers.with_raw_response.delete_by_id( - "id", + "htzv5orfit78e1m2biiifpbv", ) assert response.is_closed is True @@ -194,7 +194,7 @@ def test_raw_response_delete_by_id(self, client: Kernel) -> None: @parametrize def test_streaming_response_delete_by_id(self, client: Kernel) -> None: with client.browsers.with_streaming_response.delete_by_id( - "id", + "htzv5orfit78e1m2biiifpbv", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -267,7 +267,7 @@ async def test_streaming_response_create(self, async_client: AsyncKernel) -> Non @parametrize async def test_method_retrieve(self, async_client: AsyncKernel) -> None: browser = await async_client.browsers.retrieve( - "id", + "htzv5orfit78e1m2biiifpbv", ) assert_matches_type(BrowserRetrieveResponse, browser, path=["response"]) @@ -275,7 +275,7 @@ async def test_method_retrieve(self, async_client: AsyncKernel) -> None: @parametrize async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.with_raw_response.retrieve( - "id", + "htzv5orfit78e1m2biiifpbv", ) assert response.is_closed is True @@ -287,7 +287,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: async with async_client.browsers.with_streaming_response.retrieve( - "id", + "htzv5orfit78e1m2biiifpbv", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -371,7 +371,7 @@ async def test_streaming_response_delete(self, async_client: AsyncKernel) -> Non @parametrize async def test_method_delete_by_id(self, async_client: AsyncKernel) -> None: browser = await async_client.browsers.delete_by_id( - "id", + "htzv5orfit78e1m2biiifpbv", ) assert browser is None @@ -379,7 +379,7 @@ async def test_method_delete_by_id(self, async_client: AsyncKernel) -> None: @parametrize async def test_raw_response_delete_by_id(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.with_raw_response.delete_by_id( - "id", + "htzv5orfit78e1m2biiifpbv", ) assert response.is_closed is True @@ -391,7 +391,7 @@ async def test_raw_response_delete_by_id(self, async_client: AsyncKernel) -> Non @parametrize async def test_streaming_response_delete_by_id(self, async_client: AsyncKernel) -> None: async with async_client.browsers.with_streaming_response.delete_by_id( - "id", + "htzv5orfit78e1m2biiifpbv", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/api_resources/test_invocations.py b/tests/api_resources/test_invocations.py index 852f7f94..38734f85 100644 --- a/tests/api_resources/test_invocations.py +++ b/tests/api_resources/test_invocations.py @@ -77,7 +77,7 @@ def test_streaming_response_create(self, client: Kernel) -> None: @parametrize def test_method_retrieve(self, client: Kernel) -> None: invocation = client.invocations.retrieve( - "id", + "rr33xuugxj9h0bkf1rdt2bet", ) assert_matches_type(InvocationRetrieveResponse, invocation, path=["response"]) @@ -85,7 +85,7 @@ def test_method_retrieve(self, client: Kernel) -> None: @parametrize def test_raw_response_retrieve(self, client: Kernel) -> None: response = client.invocations.with_raw_response.retrieve( - "id", + "rr33xuugxj9h0bkf1rdt2bet", ) assert response.is_closed is True @@ -97,7 +97,7 @@ def test_raw_response_retrieve(self, client: Kernel) -> None: @parametrize def test_streaming_response_retrieve(self, client: Kernel) -> None: with client.invocations.with_streaming_response.retrieve( - "id", + "rr33xuugxj9h0bkf1rdt2bet", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -316,7 +316,7 @@ async def test_streaming_response_create(self, async_client: AsyncKernel) -> Non @parametrize async def test_method_retrieve(self, async_client: AsyncKernel) -> None: invocation = await async_client.invocations.retrieve( - "id", + "rr33xuugxj9h0bkf1rdt2bet", ) assert_matches_type(InvocationRetrieveResponse, invocation, path=["response"]) @@ -324,7 +324,7 @@ async def test_method_retrieve(self, async_client: AsyncKernel) -> None: @parametrize async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: response = await async_client.invocations.with_raw_response.retrieve( - "id", + "rr33xuugxj9h0bkf1rdt2bet", ) assert response.is_closed is True @@ -336,7 +336,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: async with async_client.invocations.with_streaming_response.retrieve( - "id", + "rr33xuugxj9h0bkf1rdt2bet", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" From 192a53d6b9ce1256d5c3fcb5314f79e7a1792491 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 17:27:56 +0000 Subject: [PATCH 167/448] feat: Add stainless CI --- .stats.yml | 8 +- api.md | 15 + src/kernel/_client.py | 10 +- src/kernel/resources/__init__.py | 14 + src/kernel/resources/browsers/browsers.py | 20 +- src/kernel/resources/invocations.py | 12 +- src/kernel/resources/proxies.py | 409 ++++++++++++++++++++ src/kernel/types/__init__.py | 4 + src/kernel/types/browser_create_params.py | 11 +- src/kernel/types/proxy_create_params.py | 175 +++++++++ src/kernel/types/proxy_create_response.py | 179 +++++++++ src/kernel/types/proxy_list_response.py | 183 +++++++++ src/kernel/types/proxy_retrieve_response.py | 179 +++++++++ tests/api_resources/test_browsers.py | 6 +- tests/api_resources/test_proxies.py | 336 ++++++++++++++++ 15 files changed, 1547 insertions(+), 14 deletions(-) create mode 100644 src/kernel/resources/proxies.py create mode 100644 src/kernel/types/proxy_create_params.py create mode 100644 src/kernel/types/proxy_create_response.py create mode 100644 src/kernel/types/proxy_list_response.py create mode 100644 src/kernel/types/proxy_retrieve_response.py create mode 100644 tests/api_resources/test_proxies.py diff --git a/.stats.yml b/.stats.yml index 7fb3d31e..385372f3 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 46 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-cb38560915edce03abce2ae3ef5bc745489dbe9b6f80c2b4ff42edf8c2ff276d.yml -openapi_spec_hash: a869194d6c864ba28d79ec0105439c3e -config_hash: ed56f95781ec9b2e73c97e1a66606071 +configured_endpoints: 50 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-d3a597bbbb25c131e2c06eb9b47d70932d14a97a6f916677a195a128e196f4db.yml +openapi_spec_hash: c967b384624017eed0abff1b53a74530 +config_hash: 0d150b61cae2dc57d3648ceae7784966 diff --git a/api.md b/api.md index 56d852bc..21ccdb73 100644 --- a/api.md +++ b/api.md @@ -178,3 +178,18 @@ Methods: - client.profiles.list() -> ProfileListResponse - client.profiles.delete(id_or_name) -> None - client.profiles.download(id_or_name) -> BinaryAPIResponse + +# Proxies + +Types: + +```python +from kernel.types import ProxyCreateResponse, ProxyRetrieveResponse, ProxyListResponse +``` + +Methods: + +- client.proxies.create(\*\*params) -> ProxyCreateResponse +- client.proxies.retrieve(id) -> ProxyRetrieveResponse +- client.proxies.list() -> ProxyListResponse +- client.proxies.delete(id) -> None diff --git a/src/kernel/_client.py b/src/kernel/_client.py index 830aeb58..2821af63 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -21,7 +21,7 @@ ) from ._utils import is_given, get_async_library from ._version import __version__ -from .resources import apps, profiles, deployments, invocations +from .resources import apps, proxies, profiles, deployments, invocations from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import KernelError, APIStatusError from ._base_client import ( @@ -55,6 +55,7 @@ class Kernel(SyncAPIClient): invocations: invocations.InvocationsResource browsers: browsers.BrowsersResource profiles: profiles.ProfilesResource + proxies: proxies.ProxiesResource with_raw_response: KernelWithRawResponse with_streaming_response: KernelWithStreamedResponse @@ -141,6 +142,7 @@ def __init__( self.invocations = invocations.InvocationsResource(self) self.browsers = browsers.BrowsersResource(self) self.profiles = profiles.ProfilesResource(self) + self.proxies = proxies.ProxiesResource(self) self.with_raw_response = KernelWithRawResponse(self) self.with_streaming_response = KernelWithStreamedResponse(self) @@ -257,6 +259,7 @@ class AsyncKernel(AsyncAPIClient): invocations: invocations.AsyncInvocationsResource browsers: browsers.AsyncBrowsersResource profiles: profiles.AsyncProfilesResource + proxies: proxies.AsyncProxiesResource with_raw_response: AsyncKernelWithRawResponse with_streaming_response: AsyncKernelWithStreamedResponse @@ -343,6 +346,7 @@ def __init__( self.invocations = invocations.AsyncInvocationsResource(self) self.browsers = browsers.AsyncBrowsersResource(self) self.profiles = profiles.AsyncProfilesResource(self) + self.proxies = proxies.AsyncProxiesResource(self) self.with_raw_response = AsyncKernelWithRawResponse(self) self.with_streaming_response = AsyncKernelWithStreamedResponse(self) @@ -460,6 +464,7 @@ def __init__(self, client: Kernel) -> None: self.invocations = invocations.InvocationsResourceWithRawResponse(client.invocations) self.browsers = browsers.BrowsersResourceWithRawResponse(client.browsers) self.profiles = profiles.ProfilesResourceWithRawResponse(client.profiles) + self.proxies = proxies.ProxiesResourceWithRawResponse(client.proxies) class AsyncKernelWithRawResponse: @@ -469,6 +474,7 @@ def __init__(self, client: AsyncKernel) -> None: self.invocations = invocations.AsyncInvocationsResourceWithRawResponse(client.invocations) self.browsers = browsers.AsyncBrowsersResourceWithRawResponse(client.browsers) self.profiles = profiles.AsyncProfilesResourceWithRawResponse(client.profiles) + self.proxies = proxies.AsyncProxiesResourceWithRawResponse(client.proxies) class KernelWithStreamedResponse: @@ -478,6 +484,7 @@ def __init__(self, client: Kernel) -> None: self.invocations = invocations.InvocationsResourceWithStreamingResponse(client.invocations) self.browsers = browsers.BrowsersResourceWithStreamingResponse(client.browsers) self.profiles = profiles.ProfilesResourceWithStreamingResponse(client.profiles) + self.proxies = proxies.ProxiesResourceWithStreamingResponse(client.proxies) class AsyncKernelWithStreamedResponse: @@ -487,6 +494,7 @@ def __init__(self, client: AsyncKernel) -> None: self.invocations = invocations.AsyncInvocationsResourceWithStreamingResponse(client.invocations) self.browsers = browsers.AsyncBrowsersResourceWithStreamingResponse(client.browsers) self.profiles = profiles.AsyncProfilesResourceWithStreamingResponse(client.profiles) + self.proxies = proxies.AsyncProxiesResourceWithStreamingResponse(client.proxies) Client = Kernel diff --git a/src/kernel/resources/__init__.py b/src/kernel/resources/__init__.py index 964da373..23b6b077 100644 --- a/src/kernel/resources/__init__.py +++ b/src/kernel/resources/__init__.py @@ -8,6 +8,14 @@ AppsResourceWithStreamingResponse, AsyncAppsResourceWithStreamingResponse, ) +from .proxies import ( + ProxiesResource, + AsyncProxiesResource, + ProxiesResourceWithRawResponse, + AsyncProxiesResourceWithRawResponse, + ProxiesResourceWithStreamingResponse, + AsyncProxiesResourceWithStreamingResponse, +) from .browsers import ( BrowsersResource, AsyncBrowsersResource, @@ -72,4 +80,10 @@ "AsyncProfilesResourceWithRawResponse", "ProfilesResourceWithStreamingResponse", "AsyncProfilesResourceWithStreamingResponse", + "ProxiesResource", + "AsyncProxiesResource", + "ProxiesResourceWithRawResponse", + "AsyncProxiesResourceWithRawResponse", + "ProxiesResourceWithStreamingResponse", + "AsyncProxiesResourceWithStreamingResponse", ] diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index e871c215..145d0831 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -99,6 +99,7 @@ def create( invocation_id: str | Omit = omit, persistence: BrowserPersistenceParam | Omit = omit, profile: browser_create_params.Profile | Omit = omit, + proxy_id: str | Omit = omit, stealth: bool | Omit = omit, timeout_seconds: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -123,12 +124,18 @@ def create( specified, the matching profile will be loaded into the browser session. Profiles must be created beforehand. + proxy_id: Optional proxy to associate to the browser session. Must reference a proxy + belonging to the caller's org. + stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. timeout_seconds: The number of seconds of inactivity before the browser session is terminated. Only applicable to non-persistent browsers. Activity includes CDP connections - and live view connections. Defaults to 60 seconds. + and live view connections. Defaults to 60 seconds. Minimum allowed is 10 + seconds. Maximum allowed is 86400 (24 hours). We check for inactivity every 5 + seconds, so the actual timeout behavior you will see is +/- 5 seconds around the + specified value. extra_headers: Send extra headers @@ -146,6 +153,7 @@ def create( "invocation_id": invocation_id, "persistence": persistence, "profile": profile, + "proxy_id": proxy_id, "stealth": stealth, "timeout_seconds": timeout_seconds, }, @@ -325,6 +333,7 @@ async def create( invocation_id: str | Omit = omit, persistence: BrowserPersistenceParam | Omit = omit, profile: browser_create_params.Profile | Omit = omit, + proxy_id: str | Omit = omit, stealth: bool | Omit = omit, timeout_seconds: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -349,12 +358,18 @@ async def create( specified, the matching profile will be loaded into the browser session. Profiles must be created beforehand. + proxy_id: Optional proxy to associate to the browser session. Must reference a proxy + belonging to the caller's org. + stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. timeout_seconds: The number of seconds of inactivity before the browser session is terminated. Only applicable to non-persistent browsers. Activity includes CDP connections - and live view connections. Defaults to 60 seconds. + and live view connections. Defaults to 60 seconds. Minimum allowed is 10 + seconds. Maximum allowed is 86400 (24 hours). We check for inactivity every 5 + seconds, so the actual timeout behavior you will see is +/- 5 seconds around the + specified value. extra_headers: Send extra headers @@ -372,6 +387,7 @@ async def create( "invocation_id": invocation_id, "persistence": persistence, "profile": profile, + "proxy_id": proxy_id, "stealth": stealth, "timeout_seconds": timeout_seconds, }, diff --git a/src/kernel/resources/invocations.py b/src/kernel/resources/invocations.py index 2a3848a0..8b5c1bc4 100644 --- a/src/kernel/resources/invocations.py +++ b/src/kernel/resources/invocations.py @@ -150,8 +150,10 @@ def update( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> InvocationUpdateResponse: - """ - Update an invocation's status or output. + """Update an invocation's status or output. + + This can used to cancel an invocation + by setting the status to "failed". Args: status: New status for the invocation. @@ -380,8 +382,10 @@ async def update( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> InvocationUpdateResponse: - """ - Update an invocation's status or output. + """Update an invocation's status or output. + + This can used to cancel an invocation + by setting the status to "failed". Args: status: New status for the invocation. diff --git a/src/kernel/resources/proxies.py b/src/kernel/resources/proxies.py new file mode 100644 index 00000000..886c423b --- /dev/null +++ b/src/kernel/resources/proxies.py @@ -0,0 +1,409 @@ +# 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 proxy_create_params +from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given +from .._utils import maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +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_list_response import ProxyListResponse +from ..types.proxy_create_response import ProxyCreateResponse +from ..types.proxy_retrieve_response import ProxyRetrieveResponse + +__all__ = ["ProxiesResource", "AsyncProxiesResource"] + + +class ProxiesResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> ProxiesResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return ProxiesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ProxiesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return ProxiesResourceWithStreamingResponse(self) + + def create( + self, + *, + type: Literal["datacenter", "isp", "residential", "mobile", "custom"], + config: proxy_create_params.Config | Omit = omit, + name: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ProxyCreateResponse: + """ + Create a new proxy configuration for the caller's organization. + + Args: + type: Proxy type to use. In terms of quality for avoiding bot-detection, from best to + worst: `mobile` > `residential` > `isp` > `datacenter`. + + config: Configuration specific to the selected proxy `type`. + + name: Readable name of the proxy. + + 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( + "/proxies", + body=maybe_transform( + { + "type": type, + "config": config, + "name": name, + }, + proxy_create_params.ProxyCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProxyCreateResponse, + ) + + def retrieve( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ProxyRetrieveResponse: + """ + Retrieve a proxy belonging to the caller's organization by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/proxies/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProxyRetrieveResponse, + ) + + def list( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ProxyListResponse: + """List proxies owned by the caller's organization.""" + return self._get( + "/proxies", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProxyListResponse, + ) + + def delete( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """Soft delete a proxy. + + Sessions referencing it are not modified. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + f"/proxies/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class AsyncProxiesResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncProxiesResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncProxiesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncProxiesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return AsyncProxiesResourceWithStreamingResponse(self) + + async def create( + self, + *, + type: Literal["datacenter", "isp", "residential", "mobile", "custom"], + config: proxy_create_params.Config | Omit = omit, + name: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ProxyCreateResponse: + """ + Create a new proxy configuration for the caller's organization. + + Args: + type: Proxy type to use. In terms of quality for avoiding bot-detection, from best to + worst: `mobile` > `residential` > `isp` > `datacenter`. + + config: Configuration specific to the selected proxy `type`. + + name: Readable name of the proxy. + + 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( + "/proxies", + body=await async_maybe_transform( + { + "type": type, + "config": config, + "name": name, + }, + proxy_create_params.ProxyCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProxyCreateResponse, + ) + + async def retrieve( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ProxyRetrieveResponse: + """ + Retrieve a proxy belonging to the caller's organization by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/proxies/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProxyRetrieveResponse, + ) + + async def list( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ProxyListResponse: + """List proxies owned by the caller's organization.""" + return await self._get( + "/proxies", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProxyListResponse, + ) + + async def delete( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """Soft delete a proxy. + + Sessions referencing it are not modified. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + f"/proxies/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class ProxiesResourceWithRawResponse: + def __init__(self, proxies: ProxiesResource) -> None: + self._proxies = proxies + + self.create = to_raw_response_wrapper( + proxies.create, + ) + self.retrieve = to_raw_response_wrapper( + proxies.retrieve, + ) + self.list = to_raw_response_wrapper( + proxies.list, + ) + self.delete = to_raw_response_wrapper( + proxies.delete, + ) + + +class AsyncProxiesResourceWithRawResponse: + def __init__(self, proxies: AsyncProxiesResource) -> None: + self._proxies = proxies + + self.create = async_to_raw_response_wrapper( + proxies.create, + ) + self.retrieve = async_to_raw_response_wrapper( + proxies.retrieve, + ) + self.list = async_to_raw_response_wrapper( + proxies.list, + ) + self.delete = async_to_raw_response_wrapper( + proxies.delete, + ) + + +class ProxiesResourceWithStreamingResponse: + def __init__(self, proxies: ProxiesResource) -> None: + self._proxies = proxies + + self.create = to_streamed_response_wrapper( + proxies.create, + ) + self.retrieve = to_streamed_response_wrapper( + proxies.retrieve, + ) + self.list = to_streamed_response_wrapper( + proxies.list, + ) + self.delete = to_streamed_response_wrapper( + proxies.delete, + ) + + +class AsyncProxiesResourceWithStreamingResponse: + def __init__(self, proxies: AsyncProxiesResource) -> None: + self._proxies = proxies + + self.create = async_to_streamed_response_wrapper( + proxies.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + proxies.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + proxies.list, + ) + self.delete = async_to_streamed_response_wrapper( + proxies.delete, + ) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 3c17469d..e571af62 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -14,15 +14,19 @@ from .app_list_params import AppListParams as AppListParams from .app_list_response import AppListResponse as AppListResponse from .browser_persistence import BrowserPersistence as BrowserPersistence +from .proxy_create_params import ProxyCreateParams as ProxyCreateParams +from .proxy_list_response import ProxyListResponse as ProxyListResponse from .browser_create_params import BrowserCreateParams as BrowserCreateParams from .browser_delete_params import BrowserDeleteParams as BrowserDeleteParams from .browser_list_response import BrowserListResponse as BrowserListResponse from .profile_create_params import ProfileCreateParams as ProfileCreateParams from .profile_list_response import ProfileListResponse as ProfileListResponse +from .proxy_create_response import ProxyCreateResponse as ProxyCreateResponse from .deployment_list_params import DeploymentListParams as DeploymentListParams from .deployment_state_event import DeploymentStateEvent as DeploymentStateEvent from .invocation_state_event import InvocationStateEvent as InvocationStateEvent from .browser_create_response import BrowserCreateResponse as BrowserCreateResponse +from .proxy_retrieve_response import ProxyRetrieveResponse as ProxyRetrieveResponse from .deployment_create_params import DeploymentCreateParams as DeploymentCreateParams from .deployment_follow_params import DeploymentFollowParams as DeploymentFollowParams from .deployment_list_response import DeploymentListResponse as DeploymentListResponse diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index b9bb3303..ed65be6f 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -29,6 +29,12 @@ class BrowserCreateParams(TypedDict, total=False): into the browser session. Profiles must be created beforehand. """ + proxy_id: str + """Optional proxy to associate to the browser session. + + Must reference a proxy belonging to the caller's org. + """ + stealth: bool """ If true, launches the browser in stealth mode to reduce detection by anti-bot @@ -39,7 +45,10 @@ class BrowserCreateParams(TypedDict, total=False): """The number of seconds of inactivity before the browser session is terminated. Only applicable to non-persistent browsers. Activity includes CDP connections - and live view connections. Defaults to 60 seconds. + and live view connections. Defaults to 60 seconds. Minimum allowed is 10 + seconds. Maximum allowed is 86400 (24 hours). We check for inactivity every 5 + seconds, so the actual timeout behavior you will see is +/- 5 seconds around the + specified value. """ diff --git a/src/kernel/types/proxy_create_params.py b/src/kernel/types/proxy_create_params.py new file mode 100644 index 00000000..7af31f4a --- /dev/null +++ b/src/kernel/types/proxy_create_params.py @@ -0,0 +1,175 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union +from typing_extensions import Literal, Required, TypeAlias, TypedDict + +__all__ = [ + "ProxyCreateParams", + "Config", + "ConfigDatacenterProxyConfig", + "ConfigIspProxyConfig", + "ConfigResidentialProxyConfig", + "ConfigMobileProxyConfig", + "ConfigCreateCustomProxyConfig", +] + + +class ProxyCreateParams(TypedDict, total=False): + type: Required[Literal["datacenter", "isp", "residential", "mobile", "custom"]] + """Proxy type to use. + + In terms of quality for avoiding bot-detection, from best to worst: `mobile` > + `residential` > `isp` > `datacenter`. + """ + + config: Config + """Configuration specific to the selected proxy `type`.""" + + name: str + """Readable name of the proxy.""" + + +class ConfigDatacenterProxyConfig(TypedDict, total=False): + country: Required[str] + """ISO 3166 country code or EU for the proxy exit node.""" + + +class ConfigIspProxyConfig(TypedDict, total=False): + country: Required[str] + """ISO 3166 country code or EU for the proxy exit node.""" + + +class ConfigResidentialProxyConfig(TypedDict, total=False): + asn: str + """Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html""" + + city: str + """City name (no spaces, e.g. + + `sanfrancisco`). If provided, `country` must also be provided. + """ + + country: str + """ISO 3166 country code or EU for the proxy exit node. + + Required if `city` is provided. + """ + + os: Literal["windows", "macos", "android"] + """Operating system of the residential device.""" + + state: str + """Two-letter state code.""" + + zip: str + """US ZIP code.""" + + +class ConfigMobileProxyConfig(TypedDict, total=False): + asn: str + """Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html""" + + carrier: Literal[ + "a1", + "aircel", + "airtel", + "att", + "celcom", + "chinamobile", + "claro", + "comcast", + "cox", + "digi", + "dt", + "docomo", + "dtac", + "etisalat", + "idea", + "kyivstar", + "meo", + "megafon", + "mtn", + "mtnza", + "mts", + "optus", + "orange", + "qwest", + "reliance_jio", + "robi", + "sprint", + "telefonica", + "telstra", + "tmobile", + "tigo", + "tim", + "verizon", + "vimpelcom", + "vodacomza", + "vodafone", + "vivo", + "zain", + "vivabo", + "telenormyanmar", + "kcelljsc", + "swisscom", + "singtel", + "asiacell", + "windit", + "cellc", + "ooredoo", + "drei", + "umobile", + "cableone", + "proximus", + "tele2", + "mobitel", + "o2", + "bouygues", + "free", + "sfr", + "digicel", + ] + """Mobile carrier.""" + + city: str + """City name (no spaces, e.g. + + `sanfrancisco`). If provided, `country` must also be provided. + """ + + country: str + """ISO 3166 country code or EU for the proxy exit node. + + Required if `city` is provided. + """ + + state: str + """Two-letter state code.""" + + zip: str + """US ZIP code.""" + + +class ConfigCreateCustomProxyConfig(TypedDict, total=False): + host: Required[str] + """Proxy host address or IP.""" + + port: Required[int] + """Proxy port.""" + + password: str + """Password for proxy authentication.""" + + username: str + """Username for proxy authentication.""" + + +Config: TypeAlias = Union[ + ConfigDatacenterProxyConfig, + ConfigIspProxyConfig, + ConfigResidentialProxyConfig, + ConfigMobileProxyConfig, + ConfigCreateCustomProxyConfig, +] diff --git a/src/kernel/types/proxy_create_response.py b/src/kernel/types/proxy_create_response.py new file mode 100644 index 00000000..42be61d1 --- /dev/null +++ b/src/kernel/types/proxy_create_response.py @@ -0,0 +1,179 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Union, Optional +from typing_extensions import Literal, TypeAlias + +from .._models import BaseModel + +__all__ = [ + "ProxyCreateResponse", + "Config", + "ConfigDatacenterProxyConfig", + "ConfigIspProxyConfig", + "ConfigResidentialProxyConfig", + "ConfigMobileProxyConfig", + "ConfigCustomProxyConfig", +] + + +class ConfigDatacenterProxyConfig(BaseModel): + country: str + """ISO 3166 country code or EU for the proxy exit node.""" + + +class ConfigIspProxyConfig(BaseModel): + country: str + """ISO 3166 country code or EU for the proxy exit node.""" + + +class ConfigResidentialProxyConfig(BaseModel): + asn: Optional[str] = None + """Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html""" + + city: Optional[str] = None + """City name (no spaces, e.g. + + `sanfrancisco`). If provided, `country` must also be provided. + """ + + country: Optional[str] = None + """ISO 3166 country code or EU for the proxy exit node. + + Required if `city` is provided. + """ + + os: Optional[Literal["windows", "macos", "android"]] = None + """Operating system of the residential device.""" + + state: Optional[str] = None + """Two-letter state code.""" + + zip: Optional[str] = None + """US ZIP code.""" + + +class ConfigMobileProxyConfig(BaseModel): + asn: Optional[str] = None + """Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html""" + + carrier: Optional[ + Literal[ + "a1", + "aircel", + "airtel", + "att", + "celcom", + "chinamobile", + "claro", + "comcast", + "cox", + "digi", + "dt", + "docomo", + "dtac", + "etisalat", + "idea", + "kyivstar", + "meo", + "megafon", + "mtn", + "mtnza", + "mts", + "optus", + "orange", + "qwest", + "reliance_jio", + "robi", + "sprint", + "telefonica", + "telstra", + "tmobile", + "tigo", + "tim", + "verizon", + "vimpelcom", + "vodacomza", + "vodafone", + "vivo", + "zain", + "vivabo", + "telenormyanmar", + "kcelljsc", + "swisscom", + "singtel", + "asiacell", + "windit", + "cellc", + "ooredoo", + "drei", + "umobile", + "cableone", + "proximus", + "tele2", + "mobitel", + "o2", + "bouygues", + "free", + "sfr", + "digicel", + ] + ] = None + """Mobile carrier.""" + + city: Optional[str] = None + """City name (no spaces, e.g. + + `sanfrancisco`). If provided, `country` must also be provided. + """ + + country: Optional[str] = None + """ISO 3166 country code or EU for the proxy exit node. + + Required if `city` is provided. + """ + + state: Optional[str] = None + """Two-letter state code.""" + + zip: Optional[str] = None + """US ZIP code.""" + + +class ConfigCustomProxyConfig(BaseModel): + host: str + """Proxy host address or IP.""" + + port: int + """Proxy port.""" + + has_password: Optional[bool] = None + """Whether the proxy has a password.""" + + username: Optional[str] = None + """Username for proxy authentication.""" + + +Config: TypeAlias = Union[ + ConfigDatacenterProxyConfig, + ConfigIspProxyConfig, + ConfigResidentialProxyConfig, + ConfigMobileProxyConfig, + ConfigCustomProxyConfig, +] + + +class ProxyCreateResponse(BaseModel): + type: Literal["datacenter", "isp", "residential", "mobile", "custom"] + """Proxy type to use. + + In terms of quality for avoiding bot-detection, from best to worst: `mobile` > + `residential` > `isp` > `datacenter`. + """ + + id: Optional[str] = None + + config: Optional[Config] = None + """Configuration specific to the selected proxy `type`.""" + + name: Optional[str] = None + """Readable name of the proxy.""" diff --git a/src/kernel/types/proxy_list_response.py b/src/kernel/types/proxy_list_response.py new file mode 100644 index 00000000..99367c84 --- /dev/null +++ b/src/kernel/types/proxy_list_response.py @@ -0,0 +1,183 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Union, Optional +from typing_extensions import Literal, TypeAlias + +from .._models import BaseModel + +__all__ = [ + "ProxyListResponse", + "ProxyListResponseItem", + "ProxyListResponseItemConfig", + "ProxyListResponseItemConfigDatacenterProxyConfig", + "ProxyListResponseItemConfigIspProxyConfig", + "ProxyListResponseItemConfigResidentialProxyConfig", + "ProxyListResponseItemConfigMobileProxyConfig", + "ProxyListResponseItemConfigCustomProxyConfig", +] + + +class ProxyListResponseItemConfigDatacenterProxyConfig(BaseModel): + country: str + """ISO 3166 country code or EU for the proxy exit node.""" + + +class ProxyListResponseItemConfigIspProxyConfig(BaseModel): + country: str + """ISO 3166 country code or EU for the proxy exit node.""" + + +class ProxyListResponseItemConfigResidentialProxyConfig(BaseModel): + asn: Optional[str] = None + """Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html""" + + city: Optional[str] = None + """City name (no spaces, e.g. + + `sanfrancisco`). If provided, `country` must also be provided. + """ + + country: Optional[str] = None + """ISO 3166 country code or EU for the proxy exit node. + + Required if `city` is provided. + """ + + os: Optional[Literal["windows", "macos", "android"]] = None + """Operating system of the residential device.""" + + state: Optional[str] = None + """Two-letter state code.""" + + zip: Optional[str] = None + """US ZIP code.""" + + +class ProxyListResponseItemConfigMobileProxyConfig(BaseModel): + asn: Optional[str] = None + """Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html""" + + carrier: Optional[ + Literal[ + "a1", + "aircel", + "airtel", + "att", + "celcom", + "chinamobile", + "claro", + "comcast", + "cox", + "digi", + "dt", + "docomo", + "dtac", + "etisalat", + "idea", + "kyivstar", + "meo", + "megafon", + "mtn", + "mtnza", + "mts", + "optus", + "orange", + "qwest", + "reliance_jio", + "robi", + "sprint", + "telefonica", + "telstra", + "tmobile", + "tigo", + "tim", + "verizon", + "vimpelcom", + "vodacomza", + "vodafone", + "vivo", + "zain", + "vivabo", + "telenormyanmar", + "kcelljsc", + "swisscom", + "singtel", + "asiacell", + "windit", + "cellc", + "ooredoo", + "drei", + "umobile", + "cableone", + "proximus", + "tele2", + "mobitel", + "o2", + "bouygues", + "free", + "sfr", + "digicel", + ] + ] = None + """Mobile carrier.""" + + city: Optional[str] = None + """City name (no spaces, e.g. + + `sanfrancisco`). If provided, `country` must also be provided. + """ + + country: Optional[str] = None + """ISO 3166 country code or EU for the proxy exit node. + + Required if `city` is provided. + """ + + state: Optional[str] = None + """Two-letter state code.""" + + zip: Optional[str] = None + """US ZIP code.""" + + +class ProxyListResponseItemConfigCustomProxyConfig(BaseModel): + host: str + """Proxy host address or IP.""" + + port: int + """Proxy port.""" + + has_password: Optional[bool] = None + """Whether the proxy has a password.""" + + username: Optional[str] = None + """Username for proxy authentication.""" + + +ProxyListResponseItemConfig: TypeAlias = Union[ + ProxyListResponseItemConfigDatacenterProxyConfig, + ProxyListResponseItemConfigIspProxyConfig, + ProxyListResponseItemConfigResidentialProxyConfig, + ProxyListResponseItemConfigMobileProxyConfig, + ProxyListResponseItemConfigCustomProxyConfig, +] + + +class ProxyListResponseItem(BaseModel): + type: Literal["datacenter", "isp", "residential", "mobile", "custom"] + """Proxy type to use. + + In terms of quality for avoiding bot-detection, from best to worst: `mobile` > + `residential` > `isp` > `datacenter`. + """ + + id: Optional[str] = None + + config: Optional[ProxyListResponseItemConfig] = None + """Configuration specific to the selected proxy `type`.""" + + name: Optional[str] = None + """Readable name of the proxy.""" + + +ProxyListResponse: TypeAlias = List[ProxyListResponseItem] diff --git a/src/kernel/types/proxy_retrieve_response.py b/src/kernel/types/proxy_retrieve_response.py new file mode 100644 index 00000000..cb4b4649 --- /dev/null +++ b/src/kernel/types/proxy_retrieve_response.py @@ -0,0 +1,179 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Union, Optional +from typing_extensions import Literal, TypeAlias + +from .._models import BaseModel + +__all__ = [ + "ProxyRetrieveResponse", + "Config", + "ConfigDatacenterProxyConfig", + "ConfigIspProxyConfig", + "ConfigResidentialProxyConfig", + "ConfigMobileProxyConfig", + "ConfigCustomProxyConfig", +] + + +class ConfigDatacenterProxyConfig(BaseModel): + country: str + """ISO 3166 country code or EU for the proxy exit node.""" + + +class ConfigIspProxyConfig(BaseModel): + country: str + """ISO 3166 country code or EU for the proxy exit node.""" + + +class ConfigResidentialProxyConfig(BaseModel): + asn: Optional[str] = None + """Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html""" + + city: Optional[str] = None + """City name (no spaces, e.g. + + `sanfrancisco`). If provided, `country` must also be provided. + """ + + country: Optional[str] = None + """ISO 3166 country code or EU for the proxy exit node. + + Required if `city` is provided. + """ + + os: Optional[Literal["windows", "macos", "android"]] = None + """Operating system of the residential device.""" + + state: Optional[str] = None + """Two-letter state code.""" + + zip: Optional[str] = None + """US ZIP code.""" + + +class ConfigMobileProxyConfig(BaseModel): + asn: Optional[str] = None + """Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html""" + + carrier: Optional[ + Literal[ + "a1", + "aircel", + "airtel", + "att", + "celcom", + "chinamobile", + "claro", + "comcast", + "cox", + "digi", + "dt", + "docomo", + "dtac", + "etisalat", + "idea", + "kyivstar", + "meo", + "megafon", + "mtn", + "mtnza", + "mts", + "optus", + "orange", + "qwest", + "reliance_jio", + "robi", + "sprint", + "telefonica", + "telstra", + "tmobile", + "tigo", + "tim", + "verizon", + "vimpelcom", + "vodacomza", + "vodafone", + "vivo", + "zain", + "vivabo", + "telenormyanmar", + "kcelljsc", + "swisscom", + "singtel", + "asiacell", + "windit", + "cellc", + "ooredoo", + "drei", + "umobile", + "cableone", + "proximus", + "tele2", + "mobitel", + "o2", + "bouygues", + "free", + "sfr", + "digicel", + ] + ] = None + """Mobile carrier.""" + + city: Optional[str] = None + """City name (no spaces, e.g. + + `sanfrancisco`). If provided, `country` must also be provided. + """ + + country: Optional[str] = None + """ISO 3166 country code or EU for the proxy exit node. + + Required if `city` is provided. + """ + + state: Optional[str] = None + """Two-letter state code.""" + + zip: Optional[str] = None + """US ZIP code.""" + + +class ConfigCustomProxyConfig(BaseModel): + host: str + """Proxy host address or IP.""" + + port: int + """Proxy port.""" + + has_password: Optional[bool] = None + """Whether the proxy has a password.""" + + username: Optional[str] = None + """Username for proxy authentication.""" + + +Config: TypeAlias = Union[ + ConfigDatacenterProxyConfig, + ConfigIspProxyConfig, + ConfigResidentialProxyConfig, + ConfigMobileProxyConfig, + ConfigCustomProxyConfig, +] + + +class ProxyRetrieveResponse(BaseModel): + type: Literal["datacenter", "isp", "residential", "mobile", "custom"] + """Proxy type to use. + + In terms of quality for avoiding bot-detection, from best to worst: `mobile` > + `residential` > `isp` > `datacenter`. + """ + + id: Optional[str] = None + + config: Optional[Config] = None + """Configuration specific to the selected proxy `type`.""" + + name: Optional[str] = None + """Readable name of the proxy.""" diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index f463cf65..349d74b4 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -39,8 +39,9 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: "name": "name", "save_changes": True, }, + proxy_id="proxy_id", stealth=True, - timeout_seconds=0, + timeout_seconds=10, ) assert_matches_type(BrowserCreateResponse, browser, path=["response"]) @@ -236,8 +237,9 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> "name": "name", "save_changes": True, }, + proxy_id="proxy_id", stealth=True, - timeout_seconds=0, + timeout_seconds=10, ) assert_matches_type(BrowserCreateResponse, browser, path=["response"]) diff --git a/tests/api_resources/test_proxies.py b/tests/api_resources/test_proxies.py new file mode 100644 index 00000000..de295bfa --- /dev/null +++ b/tests/api_resources/test_proxies.py @@ -0,0 +1,336 @@ +# 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 kernel import Kernel, AsyncKernel +from tests.utils import assert_matches_type +from kernel.types import ProxyListResponse, ProxyCreateResponse, ProxyRetrieveResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestProxies: + 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: Kernel) -> None: + proxy = client.proxies.create( + type="datacenter", + ) + assert_matches_type(ProxyCreateResponse, proxy, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_create_with_all_params(self, client: Kernel) -> None: + proxy = client.proxies.create( + type="datacenter", + config={"country": "US"}, + name="name", + ) + assert_matches_type(ProxyCreateResponse, proxy, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_create(self, client: Kernel) -> None: + response = client.proxies.with_raw_response.create( + type="datacenter", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + proxy = response.parse() + assert_matches_type(ProxyCreateResponse, proxy, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_create(self, client: Kernel) -> None: + with client.proxies.with_streaming_response.create( + type="datacenter", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + proxy = response.parse() + assert_matches_type(ProxyCreateResponse, proxy, 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: Kernel) -> None: + proxy = client.proxies.retrieve( + "id", + ) + assert_matches_type(ProxyRetrieveResponse, proxy, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: Kernel) -> None: + response = client.proxies.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + proxy = response.parse() + assert_matches_type(ProxyRetrieveResponse, proxy, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: Kernel) -> None: + with client.proxies.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + proxy = response.parse() + assert_matches_type(ProxyRetrieveResponse, proxy, 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: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.proxies.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_list(self, client: Kernel) -> None: + proxy = client.proxies.list() + assert_matches_type(ProxyListResponse, proxy, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_list(self, client: Kernel) -> None: + response = client.proxies.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + proxy = response.parse() + assert_matches_type(ProxyListResponse, proxy, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_list(self, client: Kernel) -> None: + with client.proxies.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + proxy = response.parse() + assert_matches_type(ProxyListResponse, proxy, 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: Kernel) -> None: + proxy = client.proxies.delete( + "id", + ) + assert proxy is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_delete(self, client: Kernel) -> None: + response = client.proxies.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + proxy = response.parse() + assert proxy is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_delete(self, client: Kernel) -> None: + with client.proxies.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + proxy = response.parse() + assert proxy 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: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.proxies.with_raw_response.delete( + "", + ) + + +class TestAsyncProxies: + 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: AsyncKernel) -> None: + proxy = await async_client.proxies.create( + type="datacenter", + ) + assert_matches_type(ProxyCreateResponse, proxy, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: + proxy = await async_client.proxies.create( + type="datacenter", + config={"country": "US"}, + name="name", + ) + assert_matches_type(ProxyCreateResponse, proxy, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_create(self, async_client: AsyncKernel) -> None: + response = await async_client.proxies.with_raw_response.create( + type="datacenter", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + proxy = await response.parse() + assert_matches_type(ProxyCreateResponse, proxy, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: + async with async_client.proxies.with_streaming_response.create( + type="datacenter", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + proxy = await response.parse() + assert_matches_type(ProxyCreateResponse, proxy, 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: AsyncKernel) -> None: + proxy = await async_client.proxies.retrieve( + "id", + ) + assert_matches_type(ProxyRetrieveResponse, proxy, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: + response = await async_client.proxies.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + proxy = await response.parse() + assert_matches_type(ProxyRetrieveResponse, proxy, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: + async with async_client.proxies.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + proxy = await response.parse() + assert_matches_type(ProxyRetrieveResponse, proxy, 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: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.proxies.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncKernel) -> None: + proxy = await async_client.proxies.list() + assert_matches_type(ProxyListResponse, proxy, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncKernel) -> None: + response = await async_client.proxies.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + proxy = await response.parse() + assert_matches_type(ProxyListResponse, proxy, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: + async with async_client.proxies.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + proxy = await response.parse() + assert_matches_type(ProxyListResponse, proxy, 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: AsyncKernel) -> None: + proxy = await async_client.proxies.delete( + "id", + ) + assert proxy is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: + response = await async_client.proxies.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + proxy = await response.parse() + assert proxy is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None: + async with async_client.proxies.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + proxy = await response.parse() + assert proxy 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: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.proxies.with_raw_response.delete( + "", + ) From fa49ff817c910d84936f2f0b8193ec741555a3e8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 17:43:22 +0000 Subject: [PATCH 168/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e82003f4..95e4ab67 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.11.1" + ".": "0.11.2" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 5533264c..f6c58357 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.11.1" +version = "0.11.2" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 0e02001c..a0cd88ab 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.11.1" # x-release-please-version +__version__ = "0.11.2" # x-release-please-version From 7593cfe4bcb9f6d353157724796ba980886e0b2a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 20:29:43 +0000 Subject: [PATCH 169/448] feat: Per Invocation Logs --- .stats.yml | 4 +-- api.md | 2 +- src/kernel/resources/invocations.py | 20 ++++++++++-- src/kernel/types/__init__.py | 1 + src/kernel/types/invocation_follow_params.py | 12 +++++++ tests/api_resources/test_invocations.py | 34 +++++++++++++++----- 6 files changed, 59 insertions(+), 14 deletions(-) create mode 100644 src/kernel/types/invocation_follow_params.py diff --git a/.stats.yml b/.stats.yml index 385372f3..4039b103 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 50 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-d3a597bbbb25c131e2c06eb9b47d70932d14a97a6f916677a195a128e196f4db.yml -openapi_spec_hash: c967b384624017eed0abff1b53a74530 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-5ee2116982adf46664acf84b8ba4b56ba65780983506c63d9b005dab49def757.yml +openapi_spec_hash: 42a3a519301d0e2bb2b5a71018915b55 config_hash: 0d150b61cae2dc57d3648ceae7784966 diff --git a/api.md b/api.md index 21ccdb73..eaef5078 100644 --- a/api.md +++ b/api.md @@ -57,7 +57,7 @@ Methods: - client.invocations.retrieve(id) -> InvocationRetrieveResponse - client.invocations.update(id, \*\*params) -> InvocationUpdateResponse - client.invocations.delete_browsers(id) -> None -- client.invocations.follow(id) -> InvocationFollowResponse +- client.invocations.follow(id, \*\*params) -> InvocationFollowResponse # Browsers diff --git a/src/kernel/resources/invocations.py b/src/kernel/resources/invocations.py index 8b5c1bc4..4d671646 100644 --- a/src/kernel/resources/invocations.py +++ b/src/kernel/resources/invocations.py @@ -7,7 +7,7 @@ import httpx -from ..types import invocation_create_params, invocation_update_params +from ..types import invocation_create_params, invocation_follow_params, invocation_update_params from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property @@ -223,6 +223,7 @@ def follow( self, id: str, *, + since: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -236,6 +237,8 @@ def follow( invocation reaches a terminal state. Args: + since: Show logs since the given time (RFC timestamps or durations like 5m). + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -250,7 +253,11 @@ def follow( return self._get( f"/invocations/{id}/events", options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform({"since": since}, invocation_follow_params.InvocationFollowParams), ), cast_to=cast( Any, InvocationFollowResponse @@ -455,6 +462,7 @@ async def follow( self, id: str, *, + since: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -468,6 +476,8 @@ async def follow( invocation reaches a terminal state. Args: + since: Show logs since the given time (RFC timestamps or durations like 5m). + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -482,7 +492,11 @@ async def follow( return await self._get( f"/invocations/{id}/events", options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform({"since": since}, invocation_follow_params.InvocationFollowParams), ), cast_to=cast( Any, InvocationFollowResponse diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index e571af62..0eae67cb 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -31,6 +31,7 @@ from .deployment_follow_params import DeploymentFollowParams as DeploymentFollowParams from .deployment_list_response import DeploymentListResponse as DeploymentListResponse from .invocation_create_params import InvocationCreateParams as InvocationCreateParams +from .invocation_follow_params import InvocationFollowParams as InvocationFollowParams from .invocation_update_params import InvocationUpdateParams as InvocationUpdateParams from .browser_persistence_param import BrowserPersistenceParam as BrowserPersistenceParam from .browser_retrieve_response import BrowserRetrieveResponse as BrowserRetrieveResponse diff --git a/src/kernel/types/invocation_follow_params.py b/src/kernel/types/invocation_follow_params.py new file mode 100644 index 00000000..67847810 --- /dev/null +++ b/src/kernel/types/invocation_follow_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["InvocationFollowParams"] + + +class InvocationFollowParams(TypedDict, total=False): + since: str + """Show logs since the given time (RFC timestamps or durations like 5m).""" diff --git a/tests/api_resources/test_invocations.py b/tests/api_resources/test_invocations.py index 38734f85..ae3b451b 100644 --- a/tests/api_resources/test_invocations.py +++ b/tests/api_resources/test_invocations.py @@ -217,7 +217,16 @@ def test_path_params_delete_browsers(self, client: Kernel) -> None: @parametrize def test_method_follow(self, client: Kernel) -> None: invocation_stream = client.invocations.follow( - "id", + id="id", + ) + invocation_stream.response.close() + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + def test_method_follow_with_all_params(self, client: Kernel) -> None: + invocation_stream = client.invocations.follow( + id="id", + since="2025-06-20T12:00:00Z", ) invocation_stream.response.close() @@ -225,7 +234,7 @@ def test_method_follow(self, client: Kernel) -> None: @parametrize def test_raw_response_follow(self, client: Kernel) -> None: response = client.invocations.with_raw_response.follow( - "id", + id="id", ) assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -236,7 +245,7 @@ def test_raw_response_follow(self, client: Kernel) -> None: @parametrize def test_streaming_response_follow(self, client: Kernel) -> None: with client.invocations.with_streaming_response.follow( - "id", + id="id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -251,7 +260,7 @@ def test_streaming_response_follow(self, client: Kernel) -> None: def test_path_params_follow(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): client.invocations.with_raw_response.follow( - "", + id="", ) @@ -456,7 +465,16 @@ async def test_path_params_delete_browsers(self, async_client: AsyncKernel) -> N @parametrize async def test_method_follow(self, async_client: AsyncKernel) -> None: invocation_stream = await async_client.invocations.follow( - "id", + id="id", + ) + await invocation_stream.response.aclose() + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + async def test_method_follow_with_all_params(self, async_client: AsyncKernel) -> None: + invocation_stream = await async_client.invocations.follow( + id="id", + since="2025-06-20T12:00:00Z", ) await invocation_stream.response.aclose() @@ -464,7 +482,7 @@ async def test_method_follow(self, async_client: AsyncKernel) -> None: @parametrize async def test_raw_response_follow(self, async_client: AsyncKernel) -> None: response = await async_client.invocations.with_raw_response.follow( - "id", + id="id", ) assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -475,7 +493,7 @@ async def test_raw_response_follow(self, async_client: AsyncKernel) -> None: @parametrize async def test_streaming_response_follow(self, async_client: AsyncKernel) -> None: async with async_client.invocations.with_streaming_response.follow( - "id", + id="id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -490,5 +508,5 @@ async def test_streaming_response_follow(self, async_client: AsyncKernel) -> Non async def test_path_params_follow(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): await async_client.invocations.with_raw_response.follow( - "", + id="", ) From 07607b64b2bc83af56de9675fc732b840de36672 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 20:56:38 +0000 Subject: [PATCH 170/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 95e4ab67..53327971 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.11.2" + ".": "0.11.3" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index f6c58357..e5bcc8f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.11.2" +version = "0.11.3" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index a0cd88ab..408c4e47 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.11.2" # x-release-please-version +__version__ = "0.11.3" # x-release-please-version From 48e549a7e21bd20740b620630304e8598e59090d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 18:14:00 +0000 Subject: [PATCH 171/448] feat: getInvocations endpoint --- .stats.yml | 8 +- api.md | 2 + src/kernel/resources/invocations.py | 158 ++++++++++++++++++- src/kernel/types/__init__.py | 2 + src/kernel/types/invocation_list_params.py | 33 ++++ src/kernel/types/invocation_list_response.py | 47 ++++++ tests/api_resources/test_invocations.py | 86 ++++++++++ 7 files changed, 330 insertions(+), 6 deletions(-) create mode 100644 src/kernel/types/invocation_list_params.py create mode 100644 src/kernel/types/invocation_list_response.py diff --git a/.stats.yml b/.stats.yml index 4039b103..be90d46e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 50 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-5ee2116982adf46664acf84b8ba4b56ba65780983506c63d9b005dab49def757.yml -openapi_spec_hash: 42a3a519301d0e2bb2b5a71018915b55 -config_hash: 0d150b61cae2dc57d3648ceae7784966 +configured_endpoints: 51 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-8b5a722e4964d2d1dcdc34afccb6d742e1c927cbbd622264c8734f132e31a0f5.yml +openapi_spec_hash: ed101ff177c2e962653ca65acf939336 +config_hash: 49c2ff978aaa5ccb4ce324a72f116010 diff --git a/api.md b/api.md index eaef5078..dc0a70f6 100644 --- a/api.md +++ b/api.md @@ -47,6 +47,7 @@ from kernel.types import ( InvocationCreateResponse, InvocationRetrieveResponse, InvocationUpdateResponse, + InvocationListResponse, InvocationFollowResponse, ) ``` @@ -56,6 +57,7 @@ Methods: - client.invocations.create(\*\*params) -> InvocationCreateResponse - client.invocations.retrieve(id) -> InvocationRetrieveResponse - client.invocations.update(id, \*\*params) -> InvocationUpdateResponse +- client.invocations.list(\*\*params) -> SyncOffsetPagination[InvocationListResponse] - client.invocations.delete_browsers(id) -> None - client.invocations.follow(id, \*\*params) -> InvocationFollowResponse diff --git a/src/kernel/resources/invocations.py b/src/kernel/resources/invocations.py index 4d671646..ed10cf2c 100644 --- a/src/kernel/resources/invocations.py +++ b/src/kernel/resources/invocations.py @@ -7,7 +7,7 @@ import httpx -from ..types import invocation_create_params, invocation_follow_params, invocation_update_params +from ..types import invocation_list_params, invocation_create_params, invocation_follow_params, invocation_update_params from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property @@ -19,7 +19,9 @@ async_to_streamed_response_wrapper, ) from .._streaming import Stream, AsyncStream -from .._base_client import make_request_options +from ..pagination import SyncOffsetPagination, AsyncOffsetPagination +from .._base_client import AsyncPaginator, make_request_options +from ..types.invocation_list_response import InvocationListResponse from ..types.invocation_create_response import InvocationCreateResponse from ..types.invocation_follow_response import InvocationFollowResponse from ..types.invocation_update_response import InvocationUpdateResponse @@ -185,6 +187,76 @@ def update( cast_to=InvocationUpdateResponse, ) + def list( + self, + *, + action_name: str | Omit = omit, + app_name: str | Omit = omit, + deployment_id: str | Omit = omit, + limit: int | Omit = omit, + offset: int | Omit = omit, + since: str | Omit = omit, + status: Literal["queued", "running", "succeeded", "failed"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SyncOffsetPagination[InvocationListResponse]: + """List invocations. + + Optionally filter by application name, action name, status, + deployment ID, or start time. + + Args: + action_name: Filter results by action name. + + app_name: Filter results by application name. + + deployment_id: Filter results by deployment ID. + + limit: Limit the number of invocations to return. + + offset: Offset the number of invocations to return. + + since: Show invocations that have started since the given time (RFC timestamps or + durations like 5m). + + status: Filter results by invocation status. + + 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_api_list( + "/invocations", + page=SyncOffsetPagination[InvocationListResponse], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "action_name": action_name, + "app_name": app_name, + "deployment_id": deployment_id, + "limit": limit, + "offset": offset, + "since": since, + "status": status, + }, + invocation_list_params.InvocationListParams, + ), + ), + model=InvocationListResponse, + ) + def delete_browsers( self, id: str, @@ -424,6 +496,76 @@ async def update( cast_to=InvocationUpdateResponse, ) + def list( + self, + *, + action_name: str | Omit = omit, + app_name: str | Omit = omit, + deployment_id: str | Omit = omit, + limit: int | Omit = omit, + offset: int | Omit = omit, + since: str | Omit = omit, + status: Literal["queued", "running", "succeeded", "failed"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[InvocationListResponse, AsyncOffsetPagination[InvocationListResponse]]: + """List invocations. + + Optionally filter by application name, action name, status, + deployment ID, or start time. + + Args: + action_name: Filter results by action name. + + app_name: Filter results by application name. + + deployment_id: Filter results by deployment ID. + + limit: Limit the number of invocations to return. + + offset: Offset the number of invocations to return. + + since: Show invocations that have started since the given time (RFC timestamps or + durations like 5m). + + status: Filter results by invocation status. + + 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_api_list( + "/invocations", + page=AsyncOffsetPagination[InvocationListResponse], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "action_name": action_name, + "app_name": app_name, + "deployment_id": deployment_id, + "limit": limit, + "offset": offset, + "since": since, + "status": status, + }, + invocation_list_params.InvocationListParams, + ), + ), + model=InvocationListResponse, + ) + async def delete_browsers( self, id: str, @@ -519,6 +661,9 @@ def __init__(self, invocations: InvocationsResource) -> None: self.update = to_raw_response_wrapper( invocations.update, ) + self.list = to_raw_response_wrapper( + invocations.list, + ) self.delete_browsers = to_raw_response_wrapper( invocations.delete_browsers, ) @@ -540,6 +685,9 @@ def __init__(self, invocations: AsyncInvocationsResource) -> None: self.update = async_to_raw_response_wrapper( invocations.update, ) + self.list = async_to_raw_response_wrapper( + invocations.list, + ) self.delete_browsers = async_to_raw_response_wrapper( invocations.delete_browsers, ) @@ -561,6 +709,9 @@ def __init__(self, invocations: InvocationsResource) -> None: self.update = to_streamed_response_wrapper( invocations.update, ) + self.list = to_streamed_response_wrapper( + invocations.list, + ) self.delete_browsers = to_streamed_response_wrapper( invocations.delete_browsers, ) @@ -582,6 +733,9 @@ def __init__(self, invocations: AsyncInvocationsResource) -> None: self.update = async_to_streamed_response_wrapper( invocations.update, ) + self.list = async_to_streamed_response_wrapper( + invocations.list, + ) self.delete_browsers = async_to_streamed_response_wrapper( invocations.delete_browsers, ) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 0eae67cb..b14918e9 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -24,6 +24,7 @@ from .proxy_create_response import ProxyCreateResponse as ProxyCreateResponse from .deployment_list_params import DeploymentListParams as DeploymentListParams from .deployment_state_event import DeploymentStateEvent as DeploymentStateEvent +from .invocation_list_params import InvocationListParams as InvocationListParams from .invocation_state_event import InvocationStateEvent as InvocationStateEvent from .browser_create_response import BrowserCreateResponse as BrowserCreateResponse from .proxy_retrieve_response import ProxyRetrieveResponse as ProxyRetrieveResponse @@ -32,6 +33,7 @@ from .deployment_list_response import DeploymentListResponse as DeploymentListResponse from .invocation_create_params import InvocationCreateParams as InvocationCreateParams from .invocation_follow_params import InvocationFollowParams as InvocationFollowParams +from .invocation_list_response import InvocationListResponse as InvocationListResponse from .invocation_update_params import InvocationUpdateParams as InvocationUpdateParams from .browser_persistence_param import BrowserPersistenceParam as BrowserPersistenceParam from .browser_retrieve_response import BrowserRetrieveResponse as BrowserRetrieveResponse diff --git a/src/kernel/types/invocation_list_params.py b/src/kernel/types/invocation_list_params.py new file mode 100644 index 00000000..06f75ff1 --- /dev/null +++ b/src/kernel/types/invocation_list_params.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, TypedDict + +__all__ = ["InvocationListParams"] + + +class InvocationListParams(TypedDict, total=False): + action_name: str + """Filter results by action name.""" + + app_name: str + """Filter results by application name.""" + + deployment_id: str + """Filter results by deployment ID.""" + + limit: int + """Limit the number of invocations to return.""" + + offset: int + """Offset the number of invocations to return.""" + + since: str + """ + Show invocations that have started since the given time (RFC timestamps or + durations like 5m). + """ + + status: Literal["queued", "running", "succeeded", "failed"] + """Filter results by invocation status.""" diff --git a/src/kernel/types/invocation_list_response.py b/src/kernel/types/invocation_list_response.py new file mode 100644 index 00000000..4c265856 --- /dev/null +++ b/src/kernel/types/invocation_list_response.py @@ -0,0 +1,47 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["InvocationListResponse"] + + +class InvocationListResponse(BaseModel): + id: str + """ID of the invocation""" + + action_name: str + """Name of the action invoked""" + + app_name: str + """Name of the application""" + + started_at: datetime + """RFC 3339 Nanoseconds timestamp when the invocation started""" + + status: Literal["queued", "running", "succeeded", "failed"] + """Status of the invocation""" + + finished_at: Optional[datetime] = None + """ + RFC 3339 Nanoseconds timestamp when the invocation finished (null if still + running) + """ + + output: Optional[str] = None + """Output produced by the action, rendered as a JSON string. + + This could be: string, number, boolean, array, object, or null. + """ + + payload: Optional[str] = None + """Payload provided to the invocation. + + This is a string that can be parsed as JSON. + """ + + status_reason: Optional[str] = None + """Status reason""" diff --git a/tests/api_resources/test_invocations.py b/tests/api_resources/test_invocations.py index ae3b451b..1abf41d0 100644 --- a/tests/api_resources/test_invocations.py +++ b/tests/api_resources/test_invocations.py @@ -10,10 +10,12 @@ from kernel import Kernel, AsyncKernel from tests.utils import assert_matches_type from kernel.types import ( + InvocationListResponse, InvocationCreateResponse, InvocationUpdateResponse, InvocationRetrieveResponse, ) +from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -171,6 +173,48 @@ def test_path_params_update(self, client: Kernel) -> None: status="succeeded", ) + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_list(self, client: Kernel) -> None: + invocation = client.invocations.list() + assert_matches_type(SyncOffsetPagination[InvocationListResponse], invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_list_with_all_params(self, client: Kernel) -> None: + invocation = client.invocations.list( + action_name="action_name", + app_name="app_name", + deployment_id="deployment_id", + limit=1, + offset=0, + since="2025-06-20T12:00:00Z", + status="queued", + ) + assert_matches_type(SyncOffsetPagination[InvocationListResponse], invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_list(self, client: Kernel) -> None: + response = client.invocations.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + invocation = response.parse() + assert_matches_type(SyncOffsetPagination[InvocationListResponse], invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_list(self, client: Kernel) -> None: + with client.invocations.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + invocation = response.parse() + assert_matches_type(SyncOffsetPagination[InvocationListResponse], invocation, path=["response"]) + + assert cast(Any, response.is_closed) is True + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_delete_browsers(self, client: Kernel) -> None: @@ -419,6 +463,48 @@ async def test_path_params_update(self, async_client: AsyncKernel) -> None: status="succeeded", ) + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncKernel) -> None: + invocation = await async_client.invocations.list() + assert_matches_type(AsyncOffsetPagination[InvocationListResponse], invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: + invocation = await async_client.invocations.list( + action_name="action_name", + app_name="app_name", + deployment_id="deployment_id", + limit=1, + offset=0, + since="2025-06-20T12:00:00Z", + status="queued", + ) + assert_matches_type(AsyncOffsetPagination[InvocationListResponse], invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncKernel) -> None: + response = await async_client.invocations.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + invocation = await response.parse() + assert_matches_type(AsyncOffsetPagination[InvocationListResponse], invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: + async with async_client.invocations.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + invocation = await response.parse() + assert_matches_type(AsyncOffsetPagination[InvocationListResponse], invocation, path=["response"]) + + assert cast(Any, response.is_closed) is True + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_delete_browsers(self, async_client: AsyncKernel) -> None: From 36b674ed09a78975ea18c6e672716beebdaef462 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 18:20:40 +0000 Subject: [PATCH 172/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 53327971..536ca316 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.11.3" + ".": "0.11.4" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index e5bcc8f8..c45dd071 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.11.3" +version = "0.11.4" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 408c4e47..acdc6580 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.11.3" # x-release-please-version +__version__ = "0.11.4" # x-release-please-version From 36457548cc88f5f740552164ab48fcceccdfc7af Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 26 Sep 2025 20:24:23 +0000 Subject: [PATCH 173/448] feat: Fix my incorrect grammer --- .stats.yml | 4 ++-- src/kernel/resources/invocations.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.stats.yml b/.stats.yml index be90d46e..81ff9672 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 51 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-8b5a722e4964d2d1dcdc34afccb6d742e1c927cbbd622264c8734f132e31a0f5.yml -openapi_spec_hash: ed101ff177c2e962653ca65acf939336 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-bfdb7e3d38870a8ba1628f4f83a3a719d470bf4f7fbecb67a6fad110447c9b6a.yml +openapi_spec_hash: fed29c80f9c25f8a7216b8c6de2051ab config_hash: 49c2ff978aaa5ccb4ce324a72f116010 diff --git a/src/kernel/resources/invocations.py b/src/kernel/resources/invocations.py index ed10cf2c..073314f7 100644 --- a/src/kernel/resources/invocations.py +++ b/src/kernel/resources/invocations.py @@ -154,8 +154,8 @@ def update( ) -> InvocationUpdateResponse: """Update an invocation's status or output. - This can used to cancel an invocation - by setting the status to "failed". + This can be used to cancel an + invocation by setting the status to "failed". Args: status: New status for the invocation. @@ -463,8 +463,8 @@ async def update( ) -> InvocationUpdateResponse: """Update an invocation's status or output. - This can used to cancel an invocation - by setting the status to "failed". + This can be used to cancel an + invocation by setting the status to "failed". Args: status: New status for the invocation. From e9e7d1b20d114af76cf25ab3539d03c370a76f06 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 18:11:49 +0000 Subject: [PATCH 174/448] feat: Add App Version to Invocation and add filtering on App Version --- .stats.yml | 4 ++-- src/kernel/resources/invocations.py | 8 ++++++++ src/kernel/types/invocation_list_params.py | 3 +++ src/kernel/types/invocation_list_response.py | 3 +++ src/kernel/types/invocation_retrieve_response.py | 3 +++ src/kernel/types/invocation_state_event.py | 3 +++ src/kernel/types/invocation_update_response.py | 3 +++ tests/api_resources/test_invocations.py | 2 ++ 8 files changed, 27 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 81ff9672..2b8a4b66 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 51 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-bfdb7e3d38870a8ba1628f4f83a3a719d470bf4f7fbecb67a6fad110447c9b6a.yml -openapi_spec_hash: fed29c80f9c25f8a7216b8c6de2051ab +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-da8bfd5cfb5a6d9ccb7e4edd123b49284f4eccb32fc9b6fb7165548535122e12.yml +openapi_spec_hash: fd6ded34689331831b5c077f71b5f08f config_hash: 49c2ff978aaa5ccb4ce324a72f116010 diff --git a/src/kernel/resources/invocations.py b/src/kernel/resources/invocations.py index 073314f7..4c7e7816 100644 --- a/src/kernel/resources/invocations.py +++ b/src/kernel/resources/invocations.py @@ -197,6 +197,7 @@ def list( offset: int | Omit = omit, since: str | Omit = omit, status: Literal["queued", "running", "succeeded", "failed"] | Omit = omit, + version: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -225,6 +226,8 @@ def list( status: Filter results by invocation status. + version: Filter results by application version. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -250,6 +253,7 @@ def list( "offset": offset, "since": since, "status": status, + "version": version, }, invocation_list_params.InvocationListParams, ), @@ -506,6 +510,7 @@ def list( offset: int | Omit = omit, since: str | Omit = omit, status: Literal["queued", "running", "succeeded", "failed"] | Omit = omit, + version: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -534,6 +539,8 @@ def list( status: Filter results by invocation status. + version: Filter results by application version. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -559,6 +566,7 @@ def list( "offset": offset, "since": since, "status": status, + "version": version, }, invocation_list_params.InvocationListParams, ), diff --git a/src/kernel/types/invocation_list_params.py b/src/kernel/types/invocation_list_params.py index 06f75ff1..9673f2d6 100644 --- a/src/kernel/types/invocation_list_params.py +++ b/src/kernel/types/invocation_list_params.py @@ -31,3 +31,6 @@ class InvocationListParams(TypedDict, total=False): status: Literal["queued", "running", "succeeded", "failed"] """Filter results by invocation status.""" + + version: str + """Filter results by application version.""" diff --git a/src/kernel/types/invocation_list_response.py b/src/kernel/types/invocation_list_response.py index 4c265856..e635b4d8 100644 --- a/src/kernel/types/invocation_list_response.py +++ b/src/kernel/types/invocation_list_response.py @@ -25,6 +25,9 @@ class InvocationListResponse(BaseModel): status: Literal["queued", "running", "succeeded", "failed"] """Status of the invocation""" + version: str + """Version label for the application""" + finished_at: Optional[datetime] = None """ RFC 3339 Nanoseconds timestamp when the invocation finished (null if still diff --git a/src/kernel/types/invocation_retrieve_response.py b/src/kernel/types/invocation_retrieve_response.py index 6626b53e..580424eb 100644 --- a/src/kernel/types/invocation_retrieve_response.py +++ b/src/kernel/types/invocation_retrieve_response.py @@ -25,6 +25,9 @@ class InvocationRetrieveResponse(BaseModel): status: Literal["queued", "running", "succeeded", "failed"] """Status of the invocation""" + version: str + """Version label for the application""" + finished_at: Optional[datetime] = None """ RFC 3339 Nanoseconds timestamp when the invocation finished (null if still diff --git a/src/kernel/types/invocation_state_event.py b/src/kernel/types/invocation_state_event.py index 6f30ea69..48a2fa30 100644 --- a/src/kernel/types/invocation_state_event.py +++ b/src/kernel/types/invocation_state_event.py @@ -25,6 +25,9 @@ class Invocation(BaseModel): status: Literal["queued", "running", "succeeded", "failed"] """Status of the invocation""" + version: str + """Version label for the application""" + finished_at: Optional[datetime] = None """ RFC 3339 Nanoseconds timestamp when the invocation finished (null if still diff --git a/src/kernel/types/invocation_update_response.py b/src/kernel/types/invocation_update_response.py index e0029a9c..3bcc8bc0 100644 --- a/src/kernel/types/invocation_update_response.py +++ b/src/kernel/types/invocation_update_response.py @@ -25,6 +25,9 @@ class InvocationUpdateResponse(BaseModel): status: Literal["queued", "running", "succeeded", "failed"] """Status of the invocation""" + version: str + """Version label for the application""" + finished_at: Optional[datetime] = None """ RFC 3339 Nanoseconds timestamp when the invocation finished (null if still diff --git a/tests/api_resources/test_invocations.py b/tests/api_resources/test_invocations.py index 1abf41d0..d36ea25a 100644 --- a/tests/api_resources/test_invocations.py +++ b/tests/api_resources/test_invocations.py @@ -190,6 +190,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None: offset=0, since="2025-06-20T12:00:00Z", status="queued", + version="version", ) assert_matches_type(SyncOffsetPagination[InvocationListResponse], invocation, path=["response"]) @@ -480,6 +481,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N offset=0, since="2025-06-20T12:00:00Z", status="queued", + version="version", ) assert_matches_type(AsyncOffsetPagination[InvocationListResponse], invocation, path=["response"]) From 526a815f4504e986b03044b591a37fd2d8dbf105 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 18:27:09 +0000 Subject: [PATCH 175/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 536ca316..d87cca6d 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.11.4" + ".": "0.11.5" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index c45dd071..4001f306 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.11.4" +version = "0.11.5" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index acdc6580..a8e88a16 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.11.4" # x-release-please-version +__version__ = "0.11.5" # x-release-please-version From 7b0985bc55b86c3c162c74eeabf953d7f3dabcad Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 30 Sep 2025 16:05:24 +0000 Subject: [PATCH 176/448] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 2b8a4b66..1127c15d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 51 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-da8bfd5cfb5a6d9ccb7e4edd123b49284f4eccb32fc9b6fb7165548535122e12.yml -openapi_spec_hash: fd6ded34689331831b5c077f71b5f08f +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-d79b0e3a9f9b6022bf845589a1eeff5bd7318d764a9f82e914c764fbbab5dac4.yml +openapi_spec_hash: c623d561039d0ec82f7841652ed82965 config_hash: 49c2ff978aaa5ccb4ce324a72f116010 From 7a3ba7c0cd9efa32e81d7e2fce6c9bf305318078 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 30 Sep 2025 16:48:29 +0000 Subject: [PATCH 177/448] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 1127c15d..4ada0071 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 51 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-d79b0e3a9f9b6022bf845589a1eeff5bd7318d764a9f82e914c764fbbab5dac4.yml -openapi_spec_hash: c623d561039d0ec82f7841652ed82965 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-a3d897b2f8f50d61df2555cbe888dfd2479a8a3faf9d9e2292cfdad3131485c5.yml +openapi_spec_hash: 6adc963fd957cd9f96bb16e62bdaed58 config_hash: 49c2ff978aaa5ccb4ce324a72f116010 From a1f58abb45e45f80f38c2ff6b1f906f56cbb7efd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 30 Sep 2025 23:52:31 +0000 Subject: [PATCH 178/448] feat: Return proxy ID in browsers response --- .stats.yml | 4 ++-- src/kernel/types/browser_create_response.py | 3 +++ src/kernel/types/browser_list_response.py | 3 +++ src/kernel/types/browser_retrieve_response.py | 3 +++ 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 4ada0071..a1c1d77e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 51 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-a3d897b2f8f50d61df2555cbe888dfd2479a8a3faf9d9e2292cfdad3131485c5.yml -openapi_spec_hash: 6adc963fd957cd9f96bb16e62bdaed58 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-d0090ff3ef876c554e7a1281d5cbe1666cf68aebfc60e05cb7f4302ee377b372.yml +openapi_spec_hash: 33fef541c420a28125f18cd1efc0d585 config_hash: 49c2ff978aaa5ccb4ce324a72f116010 diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index a1bc00ed..9b14e426 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -40,3 +40,6 @@ class BrowserCreateResponse(BaseModel): profile: Optional[Profile] = None """Browser profile metadata.""" + + proxy_id: Optional[str] = None + """ID of the proxy associated with this browser session, if any.""" diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index 08ddbd54..9c327201 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -42,5 +42,8 @@ class BrowserListResponseItem(BaseModel): profile: Optional[Profile] = None """Browser profile metadata.""" + proxy_id: Optional[str] = None + """ID of the proxy associated with this browser session, if any.""" + BrowserListResponse: TypeAlias = List[BrowserListResponseItem] diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index fc4c8396..29ce4c17 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -40,3 +40,6 @@ class BrowserRetrieveResponse(BaseModel): profile: Optional[Profile] = None """Browser profile metadata.""" + + proxy_id: Optional[str] = None + """ID of the proxy associated with this browser session, if any.""" From 01465a340a0a54082bb1acb5fff64fbddd7f597e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 30 Sep 2025 23:56:36 +0000 Subject: [PATCH 179/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d87cca6d..a7130553 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.11.5" + ".": "0.12.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 4001f306..04817a16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.11.5" +version = "0.12.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index a8e88a16..77b8fbde 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.11.5" # x-release-please-version +__version__ = "0.12.0" # x-release-please-version From 8d8a7333b0162c97fc740737a0c78214a929ced0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 21:39:31 +0000 Subject: [PATCH 180/448] feat: Update oAPI and data model for proxy status --- .stats.yml | 4 ++-- src/kernel/types/proxy_create_response.py | 7 +++++++ src/kernel/types/proxy_list_response.py | 7 +++++++ src/kernel/types/proxy_retrieve_response.py | 7 +++++++ 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index a1c1d77e..2223539e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 51 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-d0090ff3ef876c554e7a1281d5cbe1666cf68aebfc60e05cb7f4302ee377b372.yml -openapi_spec_hash: 33fef541c420a28125f18cd1efc0d585 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-a880f2209deafc4a011da42eb52f1dac0308d18ae1daa1d7253edc3385c9b1c4.yml +openapi_spec_hash: ae5af3810d28e49a68b12f2bb2d2af0e config_hash: 49c2ff978aaa5ccb4ce324a72f116010 diff --git a/src/kernel/types/proxy_create_response.py b/src/kernel/types/proxy_create_response.py index 42be61d1..d7759a74 100644 --- a/src/kernel/types/proxy_create_response.py +++ b/src/kernel/types/proxy_create_response.py @@ -1,6 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import Union, Optional +from datetime import datetime from typing_extensions import Literal, TypeAlias from .._models import BaseModel @@ -175,5 +176,11 @@ class ProxyCreateResponse(BaseModel): config: Optional[Config] = None """Configuration specific to the selected proxy `type`.""" + last_checked: Optional[datetime] = None + """Timestamp of the last health check performed on this proxy.""" + name: Optional[str] = None """Readable name of the proxy.""" + + status: Optional[Literal["available", "unavailable"]] = None + """Current health status of the proxy.""" diff --git a/src/kernel/types/proxy_list_response.py b/src/kernel/types/proxy_list_response.py index 99367c84..206e3b41 100644 --- a/src/kernel/types/proxy_list_response.py +++ b/src/kernel/types/proxy_list_response.py @@ -1,6 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import List, Union, Optional +from datetime import datetime from typing_extensions import Literal, TypeAlias from .._models import BaseModel @@ -176,8 +177,14 @@ class ProxyListResponseItem(BaseModel): config: Optional[ProxyListResponseItemConfig] = None """Configuration specific to the selected proxy `type`.""" + last_checked: Optional[datetime] = None + """Timestamp of the last health check performed on this proxy.""" + name: Optional[str] = None """Readable name of the proxy.""" + status: Optional[Literal["available", "unavailable"]] = None + """Current health status of the proxy.""" + ProxyListResponse: TypeAlias = List[ProxyListResponseItem] diff --git a/src/kernel/types/proxy_retrieve_response.py b/src/kernel/types/proxy_retrieve_response.py index cb4b4649..fe985d65 100644 --- a/src/kernel/types/proxy_retrieve_response.py +++ b/src/kernel/types/proxy_retrieve_response.py @@ -1,6 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import Union, Optional +from datetime import datetime from typing_extensions import Literal, TypeAlias from .._models import BaseModel @@ -175,5 +176,11 @@ class ProxyRetrieveResponse(BaseModel): config: Optional[Config] = None """Configuration specific to the selected proxy `type`.""" + last_checked: Optional[datetime] = None + """Timestamp of the last health check performed on this proxy.""" + name: Optional[str] = None """Readable name of the proxy.""" + + status: Optional[Literal["available", "unavailable"]] = None + """Current health status of the proxy.""" From 7d9ea0fdf89290e53f1dbf0da609e69af111cfa8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 17:27:34 +0000 Subject: [PATCH 181/448] feat: Http proxy --- .stats.yml | 4 ++-- src/kernel/resources/proxies.py | 8 ++++++++ src/kernel/types/proxy_create_params.py | 3 +++ src/kernel/types/proxy_create_response.py | 3 +++ src/kernel/types/proxy_list_response.py | 3 +++ src/kernel/types/proxy_retrieve_response.py | 3 +++ tests/api_resources/test_proxies.py | 2 ++ 7 files changed, 24 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 2223539e..0af2575f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 51 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-a880f2209deafc4a011da42eb52f1dac0308d18ae1daa1d7253edc3385c9b1c4.yml -openapi_spec_hash: ae5af3810d28e49a68b12f2bb2d2af0e +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-8a6175a75caa75c3de5400edf97a34e526ac3f62c63955375437461581deb0c2.yml +openapi_spec_hash: 1a880e4ce337a0e44630e6d87ef5162a config_hash: 49c2ff978aaa5ccb4ce324a72f116010 diff --git a/src/kernel/resources/proxies.py b/src/kernel/resources/proxies.py index 886c423b..ba6862f8 100644 --- a/src/kernel/resources/proxies.py +++ b/src/kernel/resources/proxies.py @@ -51,6 +51,7 @@ def create( type: Literal["datacenter", "isp", "residential", "mobile", "custom"], config: proxy_create_params.Config | Omit = omit, name: str | Omit = omit, + protocol: Literal["http", "https"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -69,6 +70,8 @@ def create( name: Readable name of the proxy. + protocol: Protocol to use for the proxy connection. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -84,6 +87,7 @@ def create( "type": type, "config": config, "name": name, + "protocol": protocol, }, proxy_create_params.ProxyCreateParams, ), @@ -207,6 +211,7 @@ async def create( type: Literal["datacenter", "isp", "residential", "mobile", "custom"], config: proxy_create_params.Config | Omit = omit, name: str | Omit = omit, + protocol: Literal["http", "https"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -225,6 +230,8 @@ async def create( name: Readable name of the proxy. + protocol: Protocol to use for the proxy connection. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -240,6 +247,7 @@ async def create( "type": type, "config": config, "name": name, + "protocol": protocol, }, proxy_create_params.ProxyCreateParams, ), diff --git a/src/kernel/types/proxy_create_params.py b/src/kernel/types/proxy_create_params.py index 7af31f4a..beb8a233 100644 --- a/src/kernel/types/proxy_create_params.py +++ b/src/kernel/types/proxy_create_params.py @@ -30,6 +30,9 @@ class ProxyCreateParams(TypedDict, total=False): name: str """Readable name of the proxy.""" + protocol: Literal["http", "https"] + """Protocol to use for the proxy connection.""" + class ConfigDatacenterProxyConfig(TypedDict, total=False): country: Required[str] diff --git a/src/kernel/types/proxy_create_response.py b/src/kernel/types/proxy_create_response.py index d7759a74..84290af8 100644 --- a/src/kernel/types/proxy_create_response.py +++ b/src/kernel/types/proxy_create_response.py @@ -182,5 +182,8 @@ class ProxyCreateResponse(BaseModel): name: Optional[str] = None """Readable name of the proxy.""" + protocol: Optional[Literal["http", "https"]] = None + """Protocol to use for the proxy connection.""" + status: Optional[Literal["available", "unavailable"]] = None """Current health status of the proxy.""" diff --git a/src/kernel/types/proxy_list_response.py b/src/kernel/types/proxy_list_response.py index 206e3b41..cd804f32 100644 --- a/src/kernel/types/proxy_list_response.py +++ b/src/kernel/types/proxy_list_response.py @@ -183,6 +183,9 @@ class ProxyListResponseItem(BaseModel): name: Optional[str] = None """Readable name of the proxy.""" + protocol: Optional[Literal["http", "https"]] = None + """Protocol to use for the proxy connection.""" + status: Optional[Literal["available", "unavailable"]] = None """Current health status of the proxy.""" diff --git a/src/kernel/types/proxy_retrieve_response.py b/src/kernel/types/proxy_retrieve_response.py index fe985d65..f70ea705 100644 --- a/src/kernel/types/proxy_retrieve_response.py +++ b/src/kernel/types/proxy_retrieve_response.py @@ -182,5 +182,8 @@ class ProxyRetrieveResponse(BaseModel): name: Optional[str] = None """Readable name of the proxy.""" + protocol: Optional[Literal["http", "https"]] = None + """Protocol to use for the proxy connection.""" + status: Optional[Literal["available", "unavailable"]] = None """Current health status of the proxy.""" diff --git a/tests/api_resources/test_proxies.py b/tests/api_resources/test_proxies.py index de295bfa..484848fe 100644 --- a/tests/api_resources/test_proxies.py +++ b/tests/api_resources/test_proxies.py @@ -32,6 +32,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: type="datacenter", config={"country": "US"}, name="name", + protocol="http", ) assert_matches_type(ProxyCreateResponse, proxy, path=["response"]) @@ -194,6 +195,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> type="datacenter", config={"country": "US"}, name="name", + protocol="http", ) assert_matches_type(ProxyCreateResponse, proxy, path=["response"]) From 1c750ac5114cc1667015e1a967f3cd5eefb48904 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 17:33:06 +0000 Subject: [PATCH 182/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index a7130553..d52d2b97 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.12.0" + ".": "0.13.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 04817a16..a44ee3cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.12.0" +version = "0.13.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 77b8fbde..eed10067 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.12.0" # x-release-please-version +__version__ = "0.13.0" # x-release-please-version From 8237c410cb05d5ed0d740b0b79772d1e862cca45 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 13:43:54 +0000 Subject: [PATCH 183/448] feat: WIP browser extensions --- .stats.yml | 8 +- api.md | 17 + src/kernel/_client.py | 10 +- src/kernel/resources/__init__.py | 14 + src/kernel/resources/browsers/browsers.py | 118 +++- src/kernel/resources/extensions.py | 539 ++++++++++++++++++ src/kernel/types/__init__.py | 7 + src/kernel/types/browser_create_params.py | 20 +- .../types/browser_upload_extensions_params.py | 26 + ...nsion_download_from_chrome_store_params.py | 15 + src/kernel/types/extension_list_response.py | 32 ++ src/kernel/types/extension_upload_params.py | 17 + src/kernel/types/extension_upload_response.py | 28 + tests/api_resources/test_browsers.py | 144 +++++ tests/api_resources/test_extensions.py | 477 ++++++++++++++++ 15 files changed, 1464 insertions(+), 8 deletions(-) create mode 100644 src/kernel/resources/extensions.py create mode 100644 src/kernel/types/browser_upload_extensions_params.py create mode 100644 src/kernel/types/extension_download_from_chrome_store_params.py create mode 100644 src/kernel/types/extension_list_response.py create mode 100644 src/kernel/types/extension_upload_params.py create mode 100644 src/kernel/types/extension_upload_response.py create mode 100644 tests/api_resources/test_extensions.py diff --git a/.stats.yml b/.stats.yml index 0af2575f..b296ff8f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 51 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-8a6175a75caa75c3de5400edf97a34e526ac3f62c63955375437461581deb0c2.yml -openapi_spec_hash: 1a880e4ce337a0e44630e6d87ef5162a -config_hash: 49c2ff978aaa5ccb4ce324a72f116010 +configured_endpoints: 57 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-936db268b3dcae5d64bd5d590506d8134304ffcbf67389eb9b1555b3febfd4cb.yml +openapi_spec_hash: 145485087adf1b28c052bacb4df68462 +config_hash: 5236f9b34e39dc1930e36a88c714abd4 diff --git a/api.md b/api.md index dc0a70f6..be7fff4e 100644 --- a/api.md +++ b/api.md @@ -82,6 +82,7 @@ Methods: - client.browsers.list() -> BrowserListResponse - client.browsers.delete(\*\*params) -> None - client.browsers.delete_by_id(id) -> None +- client.browsers.upload_extensions(id, \*\*params) -> None ## Replays @@ -195,3 +196,19 @@ Methods: - client.proxies.retrieve(id) -> ProxyRetrieveResponse - client.proxies.list() -> ProxyListResponse - client.proxies.delete(id) -> None + +# Extensions + +Types: + +```python +from kernel.types import ExtensionListResponse, ExtensionUploadResponse +``` + +Methods: + +- client.extensions.list() -> ExtensionListResponse +- client.extensions.delete(id_or_name) -> None +- client.extensions.download(id_or_name) -> BinaryAPIResponse +- client.extensions.download_from_chrome_store(\*\*params) -> BinaryAPIResponse +- client.extensions.upload(\*\*params) -> ExtensionUploadResponse diff --git a/src/kernel/_client.py b/src/kernel/_client.py index 2821af63..ea9e51b9 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -21,7 +21,7 @@ ) from ._utils import is_given, get_async_library from ._version import __version__ -from .resources import apps, proxies, profiles, deployments, invocations +from .resources import apps, proxies, profiles, extensions, deployments, invocations from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import KernelError, APIStatusError from ._base_client import ( @@ -56,6 +56,7 @@ class Kernel(SyncAPIClient): browsers: browsers.BrowsersResource profiles: profiles.ProfilesResource proxies: proxies.ProxiesResource + extensions: extensions.ExtensionsResource with_raw_response: KernelWithRawResponse with_streaming_response: KernelWithStreamedResponse @@ -143,6 +144,7 @@ def __init__( self.browsers = browsers.BrowsersResource(self) self.profiles = profiles.ProfilesResource(self) self.proxies = proxies.ProxiesResource(self) + self.extensions = extensions.ExtensionsResource(self) self.with_raw_response = KernelWithRawResponse(self) self.with_streaming_response = KernelWithStreamedResponse(self) @@ -260,6 +262,7 @@ class AsyncKernel(AsyncAPIClient): browsers: browsers.AsyncBrowsersResource profiles: profiles.AsyncProfilesResource proxies: proxies.AsyncProxiesResource + extensions: extensions.AsyncExtensionsResource with_raw_response: AsyncKernelWithRawResponse with_streaming_response: AsyncKernelWithStreamedResponse @@ -347,6 +350,7 @@ def __init__( self.browsers = browsers.AsyncBrowsersResource(self) self.profiles = profiles.AsyncProfilesResource(self) self.proxies = proxies.AsyncProxiesResource(self) + self.extensions = extensions.AsyncExtensionsResource(self) self.with_raw_response = AsyncKernelWithRawResponse(self) self.with_streaming_response = AsyncKernelWithStreamedResponse(self) @@ -465,6 +469,7 @@ def __init__(self, client: Kernel) -> None: self.browsers = browsers.BrowsersResourceWithRawResponse(client.browsers) self.profiles = profiles.ProfilesResourceWithRawResponse(client.profiles) self.proxies = proxies.ProxiesResourceWithRawResponse(client.proxies) + self.extensions = extensions.ExtensionsResourceWithRawResponse(client.extensions) class AsyncKernelWithRawResponse: @@ -475,6 +480,7 @@ def __init__(self, client: AsyncKernel) -> None: self.browsers = browsers.AsyncBrowsersResourceWithRawResponse(client.browsers) self.profiles = profiles.AsyncProfilesResourceWithRawResponse(client.profiles) self.proxies = proxies.AsyncProxiesResourceWithRawResponse(client.proxies) + self.extensions = extensions.AsyncExtensionsResourceWithRawResponse(client.extensions) class KernelWithStreamedResponse: @@ -485,6 +491,7 @@ def __init__(self, client: Kernel) -> None: self.browsers = browsers.BrowsersResourceWithStreamingResponse(client.browsers) self.profiles = profiles.ProfilesResourceWithStreamingResponse(client.profiles) self.proxies = proxies.ProxiesResourceWithStreamingResponse(client.proxies) + self.extensions = extensions.ExtensionsResourceWithStreamingResponse(client.extensions) class AsyncKernelWithStreamedResponse: @@ -495,6 +502,7 @@ def __init__(self, client: AsyncKernel) -> None: self.browsers = browsers.AsyncBrowsersResourceWithStreamingResponse(client.browsers) self.profiles = profiles.AsyncProfilesResourceWithStreamingResponse(client.profiles) self.proxies = proxies.AsyncProxiesResourceWithStreamingResponse(client.proxies) + self.extensions = extensions.AsyncExtensionsResourceWithStreamingResponse(client.extensions) Client = Kernel diff --git a/src/kernel/resources/__init__.py b/src/kernel/resources/__init__.py index 23b6b077..1b68d89f 100644 --- a/src/kernel/resources/__init__.py +++ b/src/kernel/resources/__init__.py @@ -32,6 +32,14 @@ ProfilesResourceWithStreamingResponse, AsyncProfilesResourceWithStreamingResponse, ) +from .extensions import ( + ExtensionsResource, + AsyncExtensionsResource, + ExtensionsResourceWithRawResponse, + AsyncExtensionsResourceWithRawResponse, + ExtensionsResourceWithStreamingResponse, + AsyncExtensionsResourceWithStreamingResponse, +) from .deployments import ( DeploymentsResource, AsyncDeploymentsResource, @@ -86,4 +94,10 @@ "AsyncProxiesResourceWithRawResponse", "ProxiesResourceWithStreamingResponse", "AsyncProxiesResourceWithStreamingResponse", + "ExtensionsResource", + "AsyncExtensionsResource", + "ExtensionsResourceWithRawResponse", + "AsyncExtensionsResourceWithRawResponse", + "ExtensionsResourceWithStreamingResponse", + "AsyncExtensionsResourceWithStreamingResponse", ] diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 145d0831..5e5530b1 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Mapping, Iterable, cast + import httpx from .logs import ( @@ -20,7 +22,7 @@ FsResourceWithStreamingResponse, AsyncFsResourceWithStreamingResponse, ) -from ...types import browser_create_params, browser_delete_params +from ...types import browser_create_params, browser_delete_params, browser_upload_extensions_params from .process import ( ProcessResource, AsyncProcessResource, @@ -38,7 +40,7 @@ AsyncReplaysResourceWithStreamingResponse, ) from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -95,6 +97,7 @@ def with_streaming_response(self) -> BrowsersResourceWithStreamingResponse: def create( self, *, + extensions: Iterable[browser_create_params.Extension] | Omit = omit, headless: bool | Omit = omit, invocation_id: str | Omit = omit, persistence: BrowserPersistenceParam | Omit = omit, @@ -113,6 +116,8 @@ def create( Create a new browser session from within an action. Args: + extensions: List of browser extensions to load into the session. Provide each by id or name. + headless: If true, launches the browser using a headless image (no VNC/GUI). Defaults to false. @@ -149,6 +154,7 @@ def create( "/browsers", body=maybe_transform( { + "extensions": extensions, "headless": headless, "invocation_id": invocation_id, "persistence": persistence, @@ -289,6 +295,52 @@ def delete_by_id( cast_to=NoneType, ) + def upload_extensions( + self, + id: str, + *, + extensions: Iterable[browser_upload_extensions_params.Extension], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via 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: + """ + Loads one or more unpacked extensions and restarts Chromium on the browser + instance. + + Args: + extensions: List of extensions to upload and activate + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + body = deepcopy_minimal({"extensions": extensions}) + files = extract_files(cast(Mapping[str, object], body), paths=[["extensions", "", "zip_file"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers["Content-Type"] = "multipart/form-data" + return self._post( + f"/browsers/{id}/extensions", + body=maybe_transform(body, browser_upload_extensions_params.BrowserUploadExtensionsParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + class AsyncBrowsersResource(AsyncAPIResource): @cached_property @@ -329,6 +381,7 @@ def with_streaming_response(self) -> AsyncBrowsersResourceWithStreamingResponse: async def create( self, *, + extensions: Iterable[browser_create_params.Extension] | Omit = omit, headless: bool | Omit = omit, invocation_id: str | Omit = omit, persistence: BrowserPersistenceParam | Omit = omit, @@ -347,6 +400,8 @@ async def create( Create a new browser session from within an action. Args: + extensions: List of browser extensions to load into the session. Provide each by id or name. + headless: If true, launches the browser using a headless image (no VNC/GUI). Defaults to false. @@ -383,6 +438,7 @@ async def create( "/browsers", body=await async_maybe_transform( { + "extensions": extensions, "headless": headless, "invocation_id": invocation_id, "persistence": persistence, @@ -525,6 +581,52 @@ async def delete_by_id( cast_to=NoneType, ) + async def upload_extensions( + self, + id: str, + *, + extensions: Iterable[browser_upload_extensions_params.Extension], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via 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: + """ + Loads one or more unpacked extensions and restarts Chromium on the browser + instance. + + Args: + extensions: List of extensions to upload and activate + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + body = deepcopy_minimal({"extensions": extensions}) + files = extract_files(cast(Mapping[str, object], body), paths=[["extensions", "", "zip_file"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers["Content-Type"] = "multipart/form-data" + return await self._post( + f"/browsers/{id}/extensions", + body=await async_maybe_transform(body, browser_upload_extensions_params.BrowserUploadExtensionsParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + class BrowsersResourceWithRawResponse: def __init__(self, browsers: BrowsersResource) -> None: @@ -545,6 +647,9 @@ def __init__(self, browsers: BrowsersResource) -> None: self.delete_by_id = to_raw_response_wrapper( browsers.delete_by_id, ) + self.upload_extensions = to_raw_response_wrapper( + browsers.upload_extensions, + ) @cached_property def replays(self) -> ReplaysResourceWithRawResponse: @@ -582,6 +687,9 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None: self.delete_by_id = async_to_raw_response_wrapper( browsers.delete_by_id, ) + self.upload_extensions = async_to_raw_response_wrapper( + browsers.upload_extensions, + ) @cached_property def replays(self) -> AsyncReplaysResourceWithRawResponse: @@ -619,6 +727,9 @@ def __init__(self, browsers: BrowsersResource) -> None: self.delete_by_id = to_streamed_response_wrapper( browsers.delete_by_id, ) + self.upload_extensions = to_streamed_response_wrapper( + browsers.upload_extensions, + ) @cached_property def replays(self) -> ReplaysResourceWithStreamingResponse: @@ -656,6 +767,9 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None: self.delete_by_id = async_to_streamed_response_wrapper( browsers.delete_by_id, ) + self.upload_extensions = async_to_streamed_response_wrapper( + browsers.upload_extensions, + ) @cached_property def replays(self) -> AsyncReplaysResourceWithStreamingResponse: diff --git a/src/kernel/resources/extensions.py b/src/kernel/resources/extensions.py new file mode 100644 index 00000000..2f868716 --- /dev/null +++ b/src/kernel/resources/extensions.py @@ -0,0 +1,539 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Mapping, cast +from typing_extensions import Literal + +import httpx + +from ..types import extension_upload_params, extension_download_from_chrome_store_params +from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, FileTypes, omit, not_given +from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + to_custom_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_custom_streamed_response_wrapper, + async_to_custom_raw_response_wrapper, + async_to_custom_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.extension_list_response import ExtensionListResponse +from ..types.extension_upload_response import ExtensionUploadResponse + +__all__ = ["ExtensionsResource", "AsyncExtensionsResource"] + + +class ExtensionsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> ExtensionsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return ExtensionsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ExtensionsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return ExtensionsResourceWithStreamingResponse(self) + + def list( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ExtensionListResponse: + """List extensions owned by the caller's organization.""" + return self._get( + "/extensions", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ExtensionListResponse, + ) + + def delete( + self, + id_or_name: 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 extension by its ID or by its name. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + f"/extensions/{id_or_name}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + def download( + self, + id_or_name: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> BinaryAPIResponse: + """ + Download the extension as a ZIP archive by ID or name. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + return self._get( + f"/extensions/{id_or_name}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BinaryAPIResponse, + ) + + def download_from_chrome_store( + self, + *, + url: str, + os: Literal["win", "mac", "linux"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> BinaryAPIResponse: + """ + Returns a ZIP archive containing the unpacked extension fetched from the Chrome + Web Store. + + Args: + url: Chrome Web Store URL for the extension. + + os: Target operating system for the extension package. Defaults to linux. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + return self._get( + "/extensions/from_chrome_store", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "url": url, + "os": os, + }, + extension_download_from_chrome_store_params.ExtensionDownloadFromChromeStoreParams, + ), + ), + cast_to=BinaryAPIResponse, + ) + + def upload( + self, + *, + file: FileTypes, + name: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ExtensionUploadResponse: + """Upload a zip file containing an unpacked browser extension. + + Optionally provide a + unique name for later reference. + + Args: + file: ZIP file containing the browser extension. + + name: Optional unique name within the organization to reference this extension. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "file": file, + "name": name, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return self._post( + "/extensions", + body=maybe_transform(body, extension_upload_params.ExtensionUploadParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ExtensionUploadResponse, + ) + + +class AsyncExtensionsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncExtensionsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncExtensionsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncExtensionsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return AsyncExtensionsResourceWithStreamingResponse(self) + + async def list( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ExtensionListResponse: + """List extensions owned by the caller's organization.""" + return await self._get( + "/extensions", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ExtensionListResponse, + ) + + async def delete( + self, + id_or_name: 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 extension by its ID or by its name. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + f"/extensions/{id_or_name}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + async def download( + self, + id_or_name: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncBinaryAPIResponse: + """ + Download the extension as a ZIP archive by ID or name. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + return await self._get( + f"/extensions/{id_or_name}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AsyncBinaryAPIResponse, + ) + + async def download_from_chrome_store( + self, + *, + url: str, + os: Literal["win", "mac", "linux"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncBinaryAPIResponse: + """ + Returns a ZIP archive containing the unpacked extension fetched from the Chrome + Web Store. + + Args: + url: Chrome Web Store URL for the extension. + + os: Target operating system for the extension package. Defaults to linux. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + return await self._get( + "/extensions/from_chrome_store", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "url": url, + "os": os, + }, + extension_download_from_chrome_store_params.ExtensionDownloadFromChromeStoreParams, + ), + ), + cast_to=AsyncBinaryAPIResponse, + ) + + async def upload( + self, + *, + file: FileTypes, + name: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ExtensionUploadResponse: + """Upload a zip file containing an unpacked browser extension. + + Optionally provide a + unique name for later reference. + + Args: + file: ZIP file containing the browser extension. + + name: Optional unique name within the organization to reference this extension. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "file": file, + "name": name, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return await self._post( + "/extensions", + body=await async_maybe_transform(body, extension_upload_params.ExtensionUploadParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ExtensionUploadResponse, + ) + + +class ExtensionsResourceWithRawResponse: + def __init__(self, extensions: ExtensionsResource) -> None: + self._extensions = extensions + + self.list = to_raw_response_wrapper( + extensions.list, + ) + self.delete = to_raw_response_wrapper( + extensions.delete, + ) + self.download = to_custom_raw_response_wrapper( + extensions.download, + BinaryAPIResponse, + ) + self.download_from_chrome_store = to_custom_raw_response_wrapper( + extensions.download_from_chrome_store, + BinaryAPIResponse, + ) + self.upload = to_raw_response_wrapper( + extensions.upload, + ) + + +class AsyncExtensionsResourceWithRawResponse: + def __init__(self, extensions: AsyncExtensionsResource) -> None: + self._extensions = extensions + + self.list = async_to_raw_response_wrapper( + extensions.list, + ) + self.delete = async_to_raw_response_wrapper( + extensions.delete, + ) + self.download = async_to_custom_raw_response_wrapper( + extensions.download, + AsyncBinaryAPIResponse, + ) + self.download_from_chrome_store = async_to_custom_raw_response_wrapper( + extensions.download_from_chrome_store, + AsyncBinaryAPIResponse, + ) + self.upload = async_to_raw_response_wrapper( + extensions.upload, + ) + + +class ExtensionsResourceWithStreamingResponse: + def __init__(self, extensions: ExtensionsResource) -> None: + self._extensions = extensions + + self.list = to_streamed_response_wrapper( + extensions.list, + ) + self.delete = to_streamed_response_wrapper( + extensions.delete, + ) + self.download = to_custom_streamed_response_wrapper( + extensions.download, + StreamedBinaryAPIResponse, + ) + self.download_from_chrome_store = to_custom_streamed_response_wrapper( + extensions.download_from_chrome_store, + StreamedBinaryAPIResponse, + ) + self.upload = to_streamed_response_wrapper( + extensions.upload, + ) + + +class AsyncExtensionsResourceWithStreamingResponse: + def __init__(self, extensions: AsyncExtensionsResource) -> None: + self._extensions = extensions + + self.list = async_to_streamed_response_wrapper( + extensions.list, + ) + self.delete = async_to_streamed_response_wrapper( + extensions.delete, + ) + self.download = async_to_custom_streamed_response_wrapper( + extensions.download, + AsyncStreamedBinaryAPIResponse, + ) + self.download_from_chrome_store = async_to_custom_streamed_response_wrapper( + extensions.download_from_chrome_store, + AsyncStreamedBinaryAPIResponse, + ) + self.upload = async_to_streamed_response_wrapper( + extensions.upload, + ) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index b14918e9..bc28375f 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -27,6 +27,8 @@ from .invocation_list_params import InvocationListParams as InvocationListParams from .invocation_state_event import InvocationStateEvent as InvocationStateEvent from .browser_create_response import BrowserCreateResponse as BrowserCreateResponse +from .extension_list_response import ExtensionListResponse as ExtensionListResponse +from .extension_upload_params import ExtensionUploadParams as ExtensionUploadParams from .proxy_retrieve_response import ProxyRetrieveResponse as ProxyRetrieveResponse from .deployment_create_params import DeploymentCreateParams as DeploymentCreateParams from .deployment_follow_params import DeploymentFollowParams as DeploymentFollowParams @@ -37,6 +39,7 @@ from .invocation_update_params import InvocationUpdateParams as InvocationUpdateParams from .browser_persistence_param import BrowserPersistenceParam as BrowserPersistenceParam from .browser_retrieve_response import BrowserRetrieveResponse as BrowserRetrieveResponse +from .extension_upload_response import ExtensionUploadResponse as ExtensionUploadResponse from .deployment_create_response import DeploymentCreateResponse as DeploymentCreateResponse from .deployment_follow_response import DeploymentFollowResponse as DeploymentFollowResponse from .invocation_create_response import InvocationCreateResponse as InvocationCreateResponse @@ -44,3 +47,7 @@ from .invocation_update_response import InvocationUpdateResponse as InvocationUpdateResponse from .deployment_retrieve_response import DeploymentRetrieveResponse as DeploymentRetrieveResponse from .invocation_retrieve_response import InvocationRetrieveResponse as InvocationRetrieveResponse +from .browser_upload_extensions_params import BrowserUploadExtensionsParams as BrowserUploadExtensionsParams +from .extension_download_from_chrome_store_params import ( + ExtensionDownloadFromChromeStoreParams as ExtensionDownloadFromChromeStoreParams, +) diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index ed65be6f..4a1104c8 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -2,14 +2,21 @@ from __future__ import annotations +from typing import Iterable from typing_extensions import TypedDict from .browser_persistence_param import BrowserPersistenceParam -__all__ = ["BrowserCreateParams", "Profile"] +__all__ = ["BrowserCreateParams", "Extension", "Profile"] class BrowserCreateParams(TypedDict, total=False): + extensions: Iterable[Extension] + """List of browser extensions to load into the session. + + Provide each by id or name. + """ + headless: bool """If true, launches the browser using a headless image (no VNC/GUI). @@ -52,6 +59,17 @@ class BrowserCreateParams(TypedDict, total=False): """ +class Extension(TypedDict, total=False): + id: str + """Extension ID to load for this browser session""" + + name: str + """Extension name to load for this browser session (instead of id). + + Must be 1-255 characters, using letters, numbers, dots, underscores, or hyphens. + """ + + class Profile(TypedDict, total=False): id: str """Profile ID to load for this browser session""" diff --git a/src/kernel/types/browser_upload_extensions_params.py b/src/kernel/types/browser_upload_extensions_params.py new file mode 100644 index 00000000..ab0363cb --- /dev/null +++ b/src/kernel/types/browser_upload_extensions_params.py @@ -0,0 +1,26 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Iterable +from typing_extensions import Required, TypedDict + +from .._types import FileTypes + +__all__ = ["BrowserUploadExtensionsParams", "Extension"] + + +class BrowserUploadExtensionsParams(TypedDict, total=False): + extensions: Required[Iterable[Extension]] + """List of extensions to upload and activate""" + + +class Extension(TypedDict, total=False): + name: Required[str] + """Folder name to place the extension under /home/kernel/extensions/""" + + zip_file: Required[FileTypes] + """ + Zip archive containing an unpacked Chromium extension (must include + manifest.json) + """ diff --git a/src/kernel/types/extension_download_from_chrome_store_params.py b/src/kernel/types/extension_download_from_chrome_store_params.py new file mode 100644 index 00000000..e9ca538c --- /dev/null +++ b/src/kernel/types/extension_download_from_chrome_store_params.py @@ -0,0 +1,15 @@ +# 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__ = ["ExtensionDownloadFromChromeStoreParams"] + + +class ExtensionDownloadFromChromeStoreParams(TypedDict, total=False): + url: Required[str] + """Chrome Web Store URL for the extension.""" + + os: Literal["win", "mac", "linux"] + """Target operating system for the extension package. Defaults to linux.""" diff --git a/src/kernel/types/extension_list_response.py b/src/kernel/types/extension_list_response.py new file mode 100644 index 00000000..c8c99e71 --- /dev/null +++ b/src/kernel/types/extension_list_response.py @@ -0,0 +1,32 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from datetime import datetime +from typing_extensions import TypeAlias + +from .._models import BaseModel + +__all__ = ["ExtensionListResponse", "ExtensionListResponseItem"] + + +class ExtensionListResponseItem(BaseModel): + id: str + """Unique identifier for the extension""" + + created_at: datetime + """Timestamp when the extension was created""" + + size_bytes: int + """Size of the extension archive in bytes""" + + last_used_at: Optional[datetime] = None + """Timestamp when the extension was last used""" + + name: Optional[str] = None + """Optional, easier-to-reference name for the extension. + + Must be unique within the organization. + """ + + +ExtensionListResponse: TypeAlias = List[ExtensionListResponseItem] diff --git a/src/kernel/types/extension_upload_params.py b/src/kernel/types/extension_upload_params.py new file mode 100644 index 00000000..d36dde31 --- /dev/null +++ b/src/kernel/types/extension_upload_params.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +from .._types import FileTypes + +__all__ = ["ExtensionUploadParams"] + + +class ExtensionUploadParams(TypedDict, total=False): + file: Required[FileTypes] + """ZIP file containing the browser extension.""" + + name: str + """Optional unique name within the organization to reference this extension.""" diff --git a/src/kernel/types/extension_upload_response.py b/src/kernel/types/extension_upload_response.py new file mode 100644 index 00000000..373e8861 --- /dev/null +++ b/src/kernel/types/extension_upload_response.py @@ -0,0 +1,28 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime + +from .._models import BaseModel + +__all__ = ["ExtensionUploadResponse"] + + +class ExtensionUploadResponse(BaseModel): + id: str + """Unique identifier for the extension""" + + created_at: datetime + """Timestamp when the extension was created""" + + size_bytes: int + """Size of the extension archive in bytes""" + + last_used_at: Optional[datetime] = None + """Timestamp when the extension was last used""" + + name: Optional[str] = None + """Optional, easier-to-reference name for the extension. + + Must be unique within the organization. + """ diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index 349d74b4..c3e7a7fe 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -31,6 +31,12 @@ def test_method_create(self, client: Kernel) -> None: @parametrize def test_method_create_with_all_params(self, client: Kernel) -> None: browser = client.browsers.create( + extensions=[ + { + "id": "id", + "name": "name", + } + ], headless=False, invocation_id="rr33xuugxj9h0bkf1rdt2bet", persistence={"id": "my-awesome-browser-for-user-1234"}, @@ -213,6 +219,72 @@ def test_path_params_delete_by_id(self, client: Kernel) -> None: "", ) + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_upload_extensions(self, client: Kernel) -> None: + browser = client.browsers.upload_extensions( + id="id", + extensions=[ + { + "name": "name", + "zip_file": b"raw file contents", + } + ], + ) + assert browser is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_upload_extensions(self, client: Kernel) -> None: + response = client.browsers.with_raw_response.upload_extensions( + id="id", + extensions=[ + { + "name": "name", + "zip_file": b"raw file contents", + } + ], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser = response.parse() + assert browser is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_upload_extensions(self, client: Kernel) -> None: + with client.browsers.with_streaming_response.upload_extensions( + id="id", + extensions=[ + { + "name": "name", + "zip_file": b"raw file contents", + } + ], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser = response.parse() + assert browser is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_upload_extensions(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.with_raw_response.upload_extensions( + id="", + extensions=[ + { + "name": "name", + "zip_file": b"raw file contents", + } + ], + ) + class TestAsyncBrowsers: parametrize = pytest.mark.parametrize( @@ -229,6 +301,12 @@ async def test_method_create(self, async_client: AsyncKernel) -> None: @parametrize async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: browser = await async_client.browsers.create( + extensions=[ + { + "id": "id", + "name": "name", + } + ], headless=False, invocation_id="rr33xuugxj9h0bkf1rdt2bet", persistence={"id": "my-awesome-browser-for-user-1234"}, @@ -410,3 +488,69 @@ async def test_path_params_delete_by_id(self, async_client: AsyncKernel) -> None await async_client.browsers.with_raw_response.delete_by_id( "", ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_upload_extensions(self, async_client: AsyncKernel) -> None: + browser = await async_client.browsers.upload_extensions( + id="id", + extensions=[ + { + "name": "name", + "zip_file": b"raw file contents", + } + ], + ) + assert browser is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_upload_extensions(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.with_raw_response.upload_extensions( + id="id", + extensions=[ + { + "name": "name", + "zip_file": b"raw file contents", + } + ], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser = await response.parse() + assert browser is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_upload_extensions(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.with_streaming_response.upload_extensions( + id="id", + extensions=[ + { + "name": "name", + "zip_file": b"raw file contents", + } + ], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser = await response.parse() + assert browser is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_upload_extensions(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.with_raw_response.upload_extensions( + id="", + extensions=[ + { + "name": "name", + "zip_file": b"raw file contents", + } + ], + ) diff --git a/tests/api_resources/test_extensions.py b/tests/api_resources/test_extensions.py new file mode 100644 index 00000000..5d61f327 --- /dev/null +++ b/tests/api_resources/test_extensions.py @@ -0,0 +1,477 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import httpx +import pytest +from respx import MockRouter + +from kernel import Kernel, AsyncKernel +from tests.utils import assert_matches_type +from kernel.types import ( + ExtensionListResponse, + ExtensionUploadResponse, +) +from kernel._response import ( + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestExtensions: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_list(self, client: Kernel) -> None: + extension = client.extensions.list() + assert_matches_type(ExtensionListResponse, extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_list(self, client: Kernel) -> None: + response = client.extensions.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + extension = response.parse() + assert_matches_type(ExtensionListResponse, extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_list(self, client: Kernel) -> None: + with client.extensions.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + extension = response.parse() + assert_matches_type(ExtensionListResponse, extension, 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: Kernel) -> None: + extension = client.extensions.delete( + "id_or_name", + ) + assert extension is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_delete(self, client: Kernel) -> None: + response = client.extensions.with_raw_response.delete( + "id_or_name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + extension = response.parse() + assert extension is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_delete(self, client: Kernel) -> None: + with client.extensions.with_streaming_response.delete( + "id_or_name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + extension = response.parse() + assert extension is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_delete(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): + client.extensions.with_raw_response.delete( + "", + ) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_method_download(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + extension = client.extensions.download( + "id_or_name", + ) + assert extension.is_closed + assert extension.json() == {"foo": "bar"} + assert cast(Any, extension.is_closed) is True + assert isinstance(extension, BinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_raw_response_download(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + extension = client.extensions.with_raw_response.download( + "id_or_name", + ) + + assert extension.is_closed is True + assert extension.http_request.headers.get("X-Stainless-Lang") == "python" + assert extension.json() == {"foo": "bar"} + assert isinstance(extension, BinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_streaming_response_download(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + with client.extensions.with_streaming_response.download( + "id_or_name", + ) as extension: + assert not extension.is_closed + assert extension.http_request.headers.get("X-Stainless-Lang") == "python" + + assert extension.json() == {"foo": "bar"} + assert cast(Any, extension.is_closed) is True + assert isinstance(extension, StreamedBinaryAPIResponse) + + assert cast(Any, extension.is_closed) is True + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_path_params_download(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): + client.extensions.with_raw_response.download( + "", + ) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_method_download_from_chrome_store(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/extensions/from_chrome_store").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + extension = client.extensions.download_from_chrome_store( + url="url", + ) + assert extension.is_closed + assert extension.json() == {"foo": "bar"} + assert cast(Any, extension.is_closed) is True + assert isinstance(extension, BinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_method_download_from_chrome_store_with_all_params(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/extensions/from_chrome_store").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + extension = client.extensions.download_from_chrome_store( + url="url", + os="win", + ) + assert extension.is_closed + assert extension.json() == {"foo": "bar"} + assert cast(Any, extension.is_closed) is True + assert isinstance(extension, BinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_raw_response_download_from_chrome_store(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/extensions/from_chrome_store").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + extension = client.extensions.with_raw_response.download_from_chrome_store( + url="url", + ) + + assert extension.is_closed is True + assert extension.http_request.headers.get("X-Stainless-Lang") == "python" + assert extension.json() == {"foo": "bar"} + assert isinstance(extension, BinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_streaming_response_download_from_chrome_store(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/extensions/from_chrome_store").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + with client.extensions.with_streaming_response.download_from_chrome_store( + url="url", + ) as extension: + assert not extension.is_closed + assert extension.http_request.headers.get("X-Stainless-Lang") == "python" + + assert extension.json() == {"foo": "bar"} + assert cast(Any, extension.is_closed) is True + assert isinstance(extension, StreamedBinaryAPIResponse) + + assert cast(Any, extension.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_upload(self, client: Kernel) -> None: + extension = client.extensions.upload( + file=b"raw file contents", + ) + assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_upload_with_all_params(self, client: Kernel) -> None: + extension = client.extensions.upload( + file=b"raw file contents", + name="name", + ) + assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_upload(self, client: Kernel) -> None: + response = client.extensions.with_raw_response.upload( + file=b"raw file contents", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + extension = response.parse() + assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_upload(self, client: Kernel) -> None: + with client.extensions.with_streaming_response.upload( + file=b"raw file contents", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + extension = response.parse() + assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncExtensions: + 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_list(self, async_client: AsyncKernel) -> None: + extension = await async_client.extensions.list() + assert_matches_type(ExtensionListResponse, extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncKernel) -> None: + response = await async_client.extensions.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + extension = await response.parse() + assert_matches_type(ExtensionListResponse, extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: + async with async_client.extensions.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + extension = await response.parse() + assert_matches_type(ExtensionListResponse, extension, 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: AsyncKernel) -> None: + extension = await async_client.extensions.delete( + "id_or_name", + ) + assert extension is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: + response = await async_client.extensions.with_raw_response.delete( + "id_or_name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + extension = await response.parse() + assert extension is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None: + async with async_client.extensions.with_streaming_response.delete( + "id_or_name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + extension = await response.parse() + assert extension is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_delete(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): + await async_client.extensions.with_raw_response.delete( + "", + ) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_method_download(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + extension = await async_client.extensions.download( + "id_or_name", + ) + assert extension.is_closed + assert await extension.json() == {"foo": "bar"} + assert cast(Any, extension.is_closed) is True + assert isinstance(extension, AsyncBinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_raw_response_download(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + extension = await async_client.extensions.with_raw_response.download( + "id_or_name", + ) + + assert extension.is_closed is True + assert extension.http_request.headers.get("X-Stainless-Lang") == "python" + assert await extension.json() == {"foo": "bar"} + assert isinstance(extension, AsyncBinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_streaming_response_download(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + async with async_client.extensions.with_streaming_response.download( + "id_or_name", + ) as extension: + assert not extension.is_closed + assert extension.http_request.headers.get("X-Stainless-Lang") == "python" + + assert await extension.json() == {"foo": "bar"} + assert cast(Any, extension.is_closed) is True + assert isinstance(extension, AsyncStreamedBinaryAPIResponse) + + assert cast(Any, extension.is_closed) is True + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_path_params_download(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): + await async_client.extensions.with_raw_response.download( + "", + ) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_method_download_from_chrome_store(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/extensions/from_chrome_store").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + extension = await async_client.extensions.download_from_chrome_store( + url="url", + ) + assert extension.is_closed + assert await extension.json() == {"foo": "bar"} + assert cast(Any, extension.is_closed) is True + assert isinstance(extension, AsyncBinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_method_download_from_chrome_store_with_all_params( + self, async_client: AsyncKernel, respx_mock: MockRouter + ) -> None: + respx_mock.get("/extensions/from_chrome_store").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + extension = await async_client.extensions.download_from_chrome_store( + url="url", + os="win", + ) + assert extension.is_closed + assert await extension.json() == {"foo": "bar"} + assert cast(Any, extension.is_closed) is True + assert isinstance(extension, AsyncBinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_raw_response_download_from_chrome_store( + self, async_client: AsyncKernel, respx_mock: MockRouter + ) -> None: + respx_mock.get("/extensions/from_chrome_store").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + extension = await async_client.extensions.with_raw_response.download_from_chrome_store( + url="url", + ) + + assert extension.is_closed is True + assert extension.http_request.headers.get("X-Stainless-Lang") == "python" + assert await extension.json() == {"foo": "bar"} + assert isinstance(extension, AsyncBinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_streaming_response_download_from_chrome_store( + self, async_client: AsyncKernel, respx_mock: MockRouter + ) -> None: + respx_mock.get("/extensions/from_chrome_store").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + async with async_client.extensions.with_streaming_response.download_from_chrome_store( + url="url", + ) as extension: + assert not extension.is_closed + assert extension.http_request.headers.get("X-Stainless-Lang") == "python" + + assert await extension.json() == {"foo": "bar"} + assert cast(Any, extension.is_closed) is True + assert isinstance(extension, AsyncStreamedBinaryAPIResponse) + + assert cast(Any, extension.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_upload(self, async_client: AsyncKernel) -> None: + extension = await async_client.extensions.upload( + file=b"raw file contents", + ) + assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_upload_with_all_params(self, async_client: AsyncKernel) -> None: + extension = await async_client.extensions.upload( + file=b"raw file contents", + name="name", + ) + assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_upload(self, async_client: AsyncKernel) -> None: + response = await async_client.extensions.with_raw_response.upload( + file=b"raw file contents", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + extension = await response.parse() + assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_upload(self, async_client: AsyncKernel) -> None: + async with async_client.extensions.with_streaming_response.upload( + file=b"raw file contents", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + extension = await response.parse() + assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) + + assert cast(Any, response.is_closed) is True From 110ec91fcba2c3994b3ce1882003fa993efec241 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 13:51:54 +0000 Subject: [PATCH 184/448] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index b296ff8f..f577dd04 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 57 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-936db268b3dcae5d64bd5d590506d8134304ffcbf67389eb9b1555b3febfd4cb.yml openapi_spec_hash: 145485087adf1b28c052bacb4df68462 -config_hash: 5236f9b34e39dc1930e36a88c714abd4 +config_hash: 15cd063f8e308686ac71bf9ee9634625 From 41f6dcc0ca8e1a64289d9ae109302845bb6ea160 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 14:11:19 +0000 Subject: [PATCH 185/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d52d2b97..a26ebfc1 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.13.0" + ".": "0.14.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index a44ee3cb..3219e080 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.13.0" +version = "0.14.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index eed10067..0bebaf17 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.13.0" # x-release-please-version +__version__ = "0.14.0" # x-release-please-version From f55a3d4cea46d456cd82ddec374198496596478f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 15:01:47 +0000 Subject: [PATCH 186/448] feat: Hide and deprecate mobile proxy type --- .stats.yml | 4 ++-- src/kernel/types/proxy_create_params.py | 14 ++++---------- src/kernel/types/proxy_create_response.py | 14 ++++---------- src/kernel/types/proxy_list_response.py | 14 ++++---------- src/kernel/types/proxy_retrieve_response.py | 14 ++++---------- 5 files changed, 18 insertions(+), 42 deletions(-) diff --git a/.stats.yml b/.stats.yml index f577dd04..136236ae 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 57 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-936db268b3dcae5d64bd5d590506d8134304ffcbf67389eb9b1555b3febfd4cb.yml -openapi_spec_hash: 145485087adf1b28c052bacb4df68462 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-592ab7a96084f7241d77b4cc1ce2a074795b0dc40d8247e1a0129fe3f89c1ed4.yml +openapi_spec_hash: 3a23b4c9c05946251be45c5c4e7a415d config_hash: 15cd063f8e308686ac71bf9ee9634625 diff --git a/src/kernel/types/proxy_create_params.py b/src/kernel/types/proxy_create_params.py index beb8a233..1f8d4b7d 100644 --- a/src/kernel/types/proxy_create_params.py +++ b/src/kernel/types/proxy_create_params.py @@ -36,12 +36,12 @@ class ProxyCreateParams(TypedDict, total=False): class ConfigDatacenterProxyConfig(TypedDict, total=False): country: Required[str] - """ISO 3166 country code or EU for the proxy exit node.""" + """ISO 3166 country code.""" class ConfigIspProxyConfig(TypedDict, total=False): country: Required[str] - """ISO 3166 country code or EU for the proxy exit node.""" + """ISO 3166 country code.""" class ConfigResidentialProxyConfig(TypedDict, total=False): @@ -55,10 +55,7 @@ class ConfigResidentialProxyConfig(TypedDict, total=False): """ country: str - """ISO 3166 country code or EU for the proxy exit node. - - Required if `city` is provided. - """ + """ISO 3166 country code.""" os: Literal["windows", "macos", "android"] """Operating system of the residential device.""" @@ -143,10 +140,7 @@ class ConfigMobileProxyConfig(TypedDict, total=False): """ country: str - """ISO 3166 country code or EU for the proxy exit node. - - Required if `city` is provided. - """ + """ISO 3166 country code""" state: str """Two-letter state code.""" diff --git a/src/kernel/types/proxy_create_response.py b/src/kernel/types/proxy_create_response.py index 84290af8..831c45f3 100644 --- a/src/kernel/types/proxy_create_response.py +++ b/src/kernel/types/proxy_create_response.py @@ -19,12 +19,12 @@ class ConfigDatacenterProxyConfig(BaseModel): country: str - """ISO 3166 country code or EU for the proxy exit node.""" + """ISO 3166 country code.""" class ConfigIspProxyConfig(BaseModel): country: str - """ISO 3166 country code or EU for the proxy exit node.""" + """ISO 3166 country code.""" class ConfigResidentialProxyConfig(BaseModel): @@ -38,10 +38,7 @@ class ConfigResidentialProxyConfig(BaseModel): """ country: Optional[str] = None - """ISO 3166 country code or EU for the proxy exit node. - - Required if `city` is provided. - """ + """ISO 3166 country code.""" os: Optional[Literal["windows", "macos", "android"]] = None """Operating system of the residential device.""" @@ -128,10 +125,7 @@ class ConfigMobileProxyConfig(BaseModel): """ country: Optional[str] = None - """ISO 3166 country code or EU for the proxy exit node. - - Required if `city` is provided. - """ + """ISO 3166 country code""" state: Optional[str] = None """Two-letter state code.""" diff --git a/src/kernel/types/proxy_list_response.py b/src/kernel/types/proxy_list_response.py index cd804f32..96488816 100644 --- a/src/kernel/types/proxy_list_response.py +++ b/src/kernel/types/proxy_list_response.py @@ -20,12 +20,12 @@ class ProxyListResponseItemConfigDatacenterProxyConfig(BaseModel): country: str - """ISO 3166 country code or EU for the proxy exit node.""" + """ISO 3166 country code.""" class ProxyListResponseItemConfigIspProxyConfig(BaseModel): country: str - """ISO 3166 country code or EU for the proxy exit node.""" + """ISO 3166 country code.""" class ProxyListResponseItemConfigResidentialProxyConfig(BaseModel): @@ -39,10 +39,7 @@ class ProxyListResponseItemConfigResidentialProxyConfig(BaseModel): """ country: Optional[str] = None - """ISO 3166 country code or EU for the proxy exit node. - - Required if `city` is provided. - """ + """ISO 3166 country code.""" os: Optional[Literal["windows", "macos", "android"]] = None """Operating system of the residential device.""" @@ -129,10 +126,7 @@ class ProxyListResponseItemConfigMobileProxyConfig(BaseModel): """ country: Optional[str] = None - """ISO 3166 country code or EU for the proxy exit node. - - Required if `city` is provided. - """ + """ISO 3166 country code""" state: Optional[str] = None """Two-letter state code.""" diff --git a/src/kernel/types/proxy_retrieve_response.py b/src/kernel/types/proxy_retrieve_response.py index f70ea705..4c2d63cc 100644 --- a/src/kernel/types/proxy_retrieve_response.py +++ b/src/kernel/types/proxy_retrieve_response.py @@ -19,12 +19,12 @@ class ConfigDatacenterProxyConfig(BaseModel): country: str - """ISO 3166 country code or EU for the proxy exit node.""" + """ISO 3166 country code.""" class ConfigIspProxyConfig(BaseModel): country: str - """ISO 3166 country code or EU for the proxy exit node.""" + """ISO 3166 country code.""" class ConfigResidentialProxyConfig(BaseModel): @@ -38,10 +38,7 @@ class ConfigResidentialProxyConfig(BaseModel): """ country: Optional[str] = None - """ISO 3166 country code or EU for the proxy exit node. - - Required if `city` is provided. - """ + """ISO 3166 country code.""" os: Optional[Literal["windows", "macos", "android"]] = None """Operating system of the residential device.""" @@ -128,10 +125,7 @@ class ConfigMobileProxyConfig(BaseModel): """ country: Optional[str] = None - """ISO 3166 country code or EU for the proxy exit node. - - Required if `city` is provided. - """ + """ISO 3166 country code""" state: Optional[str] = None """Two-letter state code.""" From 4fb82ef5d81c9bad05d0911df6bf7ca2fffee6b2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 11 Oct 2025 02:18:02 +0000 Subject: [PATCH 187/448] chore(internal): detect missing future annotations with ruff --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 3219e080..b06cd8f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -224,6 +224,8 @@ select = [ "B", # remove unused imports "F401", + # check for missing future annotations + "FA102", # bare except statements "E722", # unused arguments @@ -246,6 +248,8 @@ unfixable = [ "T203", ] +extend-safe-fixes = ["FA102"] + [tool.ruff.lint.flake8-tidy-imports.banned-api] "functools.lru_cache".msg = "This function does not retain type information for the wrapped function's arguments; The `lru_cache` function from `_utils` should be used instead" From 14fa666641b0d000cb911ba46a4ffb642cd33d2d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 15:40:53 +0000 Subject: [PATCH 188/448] feat: WIP: Configurable Viewport --- .stats.yml | 6 +-- api.md | 2 +- src/kernel/resources/browsers/browsers.py | 52 +++++++++++++------ src/kernel/types/__init__.py | 2 +- src/kernel/types/browser_create_params.py | 30 ++++++++++- src/kernel/types/browser_create_response.py | 28 +++++++++- src/kernel/types/browser_list_response.py | 28 +++++++++- ...s.py => browser_load_extensions_params.py} | 4 +- src/kernel/types/browser_retrieve_response.py | 28 +++++++++- tests/api_resources/test_browsers.py | 42 +++++++++------ 10 files changed, 179 insertions(+), 43 deletions(-) rename src/kernel/types/{browser_upload_extensions_params.py => browser_load_extensions_params.py} (84%) diff --git a/.stats.yml b/.stats.yml index 136236ae..99609550 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 57 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-592ab7a96084f7241d77b4cc1ce2a074795b0dc40d8247e1a0129fe3f89c1ed4.yml -openapi_spec_hash: 3a23b4c9c05946251be45c5c4e7a415d -config_hash: 15cd063f8e308686ac71bf9ee9634625 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-1cd328ccf61f0e888d6df27b091c30b38c392ab9ca8ce7fd0ead8f10aaf71ffa.yml +openapi_spec_hash: af761c48d1955f11822f3b95f9c46750 +config_hash: deadfc4d2b0a947673bcf559b5db6e1b diff --git a/api.md b/api.md index be7fff4e..6059a78d 100644 --- a/api.md +++ b/api.md @@ -82,7 +82,7 @@ Methods: - client.browsers.list() -> BrowserListResponse - client.browsers.delete(\*\*params) -> None - client.browsers.delete_by_id(id) -> None -- client.browsers.upload_extensions(id, \*\*params) -> None +- client.browsers.load_extensions(id, \*\*params) -> None ## Replays diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 5e5530b1..3e3eb8df 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -22,7 +22,7 @@ FsResourceWithStreamingResponse, AsyncFsResourceWithStreamingResponse, ) -from ...types import browser_create_params, browser_delete_params, browser_upload_extensions_params +from ...types import browser_create_params, browser_delete_params, browser_load_extensions_params from .process import ( ProcessResource, AsyncProcessResource, @@ -105,6 +105,7 @@ def create( proxy_id: str | Omit = omit, stealth: bool | Omit = omit, timeout_seconds: int | Omit = omit, + viewport: browser_create_params.Viewport | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -142,6 +143,15 @@ def create( seconds, so the actual timeout behavior you will see is +/- 5 seconds around the specified value. + viewport: Initial browser window size in pixels with optional refresh rate. If omitted, + image defaults apply (commonly 1024x768@60). Only specific viewport + configurations are supported. The server will reject unsupported combinations. + Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1024x768@60 If refresh_rate is not provided, it will be + automatically determined from the width and height if they match a supported + configuration exactly. Note: Higher resolutions may affect the responsiveness of + live view browser + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -162,6 +172,7 @@ def create( "proxy_id": proxy_id, "stealth": stealth, "timeout_seconds": timeout_seconds, + "viewport": viewport, }, browser_create_params.BrowserCreateParams, ), @@ -295,11 +306,11 @@ def delete_by_id( cast_to=NoneType, ) - def upload_extensions( + def load_extensions( self, id: str, *, - extensions: Iterable[browser_upload_extensions_params.Extension], + extensions: Iterable[browser_load_extensions_params.Extension], # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -333,7 +344,7 @@ def upload_extensions( extra_headers["Content-Type"] = "multipart/form-data" return self._post( f"/browsers/{id}/extensions", - body=maybe_transform(body, browser_upload_extensions_params.BrowserUploadExtensionsParams), + body=maybe_transform(body, browser_load_extensions_params.BrowserLoadExtensionsParams), files=files, options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -389,6 +400,7 @@ async def create( proxy_id: str | Omit = omit, stealth: bool | Omit = omit, timeout_seconds: int | Omit = omit, + viewport: browser_create_params.Viewport | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -426,6 +438,15 @@ async def create( seconds, so the actual timeout behavior you will see is +/- 5 seconds around the specified value. + viewport: Initial browser window size in pixels with optional refresh rate. If omitted, + image defaults apply (commonly 1024x768@60). Only specific viewport + configurations are supported. The server will reject unsupported combinations. + Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1024x768@60 If refresh_rate is not provided, it will be + automatically determined from the width and height if they match a supported + configuration exactly. Note: Higher resolutions may affect the responsiveness of + live view browser + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -446,6 +467,7 @@ async def create( "proxy_id": proxy_id, "stealth": stealth, "timeout_seconds": timeout_seconds, + "viewport": viewport, }, browser_create_params.BrowserCreateParams, ), @@ -581,11 +603,11 @@ async def delete_by_id( cast_to=NoneType, ) - async def upload_extensions( + async def load_extensions( self, id: str, *, - extensions: Iterable[browser_upload_extensions_params.Extension], + extensions: Iterable[browser_load_extensions_params.Extension], # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -619,7 +641,7 @@ async def upload_extensions( extra_headers["Content-Type"] = "multipart/form-data" return await self._post( f"/browsers/{id}/extensions", - body=await async_maybe_transform(body, browser_upload_extensions_params.BrowserUploadExtensionsParams), + body=await async_maybe_transform(body, browser_load_extensions_params.BrowserLoadExtensionsParams), files=files, options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -647,8 +669,8 @@ def __init__(self, browsers: BrowsersResource) -> None: self.delete_by_id = to_raw_response_wrapper( browsers.delete_by_id, ) - self.upload_extensions = to_raw_response_wrapper( - browsers.upload_extensions, + self.load_extensions = to_raw_response_wrapper( + browsers.load_extensions, ) @cached_property @@ -687,8 +709,8 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None: self.delete_by_id = async_to_raw_response_wrapper( browsers.delete_by_id, ) - self.upload_extensions = async_to_raw_response_wrapper( - browsers.upload_extensions, + self.load_extensions = async_to_raw_response_wrapper( + browsers.load_extensions, ) @cached_property @@ -727,8 +749,8 @@ def __init__(self, browsers: BrowsersResource) -> None: self.delete_by_id = to_streamed_response_wrapper( browsers.delete_by_id, ) - self.upload_extensions = to_streamed_response_wrapper( - browsers.upload_extensions, + self.load_extensions = to_streamed_response_wrapper( + browsers.load_extensions, ) @cached_property @@ -767,8 +789,8 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None: self.delete_by_id = async_to_streamed_response_wrapper( browsers.delete_by_id, ) - self.upload_extensions = async_to_streamed_response_wrapper( - browsers.upload_extensions, + self.load_extensions = async_to_streamed_response_wrapper( + browsers.load_extensions, ) @cached_property diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index bc28375f..6b49cf7f 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -47,7 +47,7 @@ from .invocation_update_response import InvocationUpdateResponse as InvocationUpdateResponse from .deployment_retrieve_response import DeploymentRetrieveResponse as DeploymentRetrieveResponse from .invocation_retrieve_response import InvocationRetrieveResponse as InvocationRetrieveResponse -from .browser_upload_extensions_params import BrowserUploadExtensionsParams as BrowserUploadExtensionsParams +from .browser_load_extensions_params import BrowserLoadExtensionsParams as BrowserLoadExtensionsParams from .extension_download_from_chrome_store_params import ( ExtensionDownloadFromChromeStoreParams as ExtensionDownloadFromChromeStoreParams, ) diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index 4a1104c8..a0214a51 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -3,11 +3,11 @@ from __future__ import annotations from typing import Iterable -from typing_extensions import TypedDict +from typing_extensions import Required, TypedDict from .browser_persistence_param import BrowserPersistenceParam -__all__ = ["BrowserCreateParams", "Extension", "Profile"] +__all__ = ["BrowserCreateParams", "Extension", "Profile", "Viewport"] class BrowserCreateParams(TypedDict, total=False): @@ -58,6 +58,18 @@ class BrowserCreateParams(TypedDict, total=False): specified value. """ + viewport: Viewport + """Initial browser window size in pixels with optional refresh rate. + + If omitted, image defaults apply (commonly 1024x768@60). Only specific viewport + configurations are supported. The server will reject unsupported combinations. + Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1024x768@60 If refresh_rate is not provided, it will be + automatically determined from the width and height if they match a supported + configuration exactly. Note: Higher resolutions may affect the responsiveness of + live view browser + """ + class Extension(TypedDict, total=False): id: str @@ -85,3 +97,17 @@ class Profile(TypedDict, total=False): If true, save changes made during the session back to the profile when the session ends. """ + + +class Viewport(TypedDict, total=False): + height: Required[int] + """Browser window height in pixels.""" + + width: Required[int] + """Browser window width in pixels.""" + + refresh_rate: int + """Display refresh rate in Hz. + + If omitted, automatically determined from width and height. + """ diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index 9b14e426..d7ef603c 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -7,7 +7,21 @@ from .._models import BaseModel from .browser_persistence import BrowserPersistence -__all__ = ["BrowserCreateResponse"] +__all__ = ["BrowserCreateResponse", "Viewport"] + + +class Viewport(BaseModel): + height: int + """Browser window height in pixels.""" + + width: int + """Browser window width in pixels.""" + + refresh_rate: Optional[int] = None + """Display refresh rate in Hz. + + If omitted, automatically determined from width and height. + """ class BrowserCreateResponse(BaseModel): @@ -43,3 +57,15 @@ class BrowserCreateResponse(BaseModel): proxy_id: Optional[str] = None """ID of the proxy associated with this browser session, if any.""" + + viewport: Optional[Viewport] = None + """Initial browser window size in pixels with optional refresh rate. + + If omitted, image defaults apply (commonly 1024x768@60). Only specific viewport + configurations are supported. The server will reject unsupported combinations. + Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1024x768@60 If refresh_rate is not provided, it will be + automatically determined from the width and height if they match a supported + configuration exactly. Note: Higher resolutions may affect the responsiveness of + live view browser + """ diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index 9c327201..22d72e18 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -8,7 +8,21 @@ from .._models import BaseModel from .browser_persistence import BrowserPersistence -__all__ = ["BrowserListResponse", "BrowserListResponseItem"] +__all__ = ["BrowserListResponse", "BrowserListResponseItem", "BrowserListResponseItemViewport"] + + +class BrowserListResponseItemViewport(BaseModel): + height: int + """Browser window height in pixels.""" + + width: int + """Browser window width in pixels.""" + + refresh_rate: Optional[int] = None + """Display refresh rate in Hz. + + If omitted, automatically determined from width and height. + """ class BrowserListResponseItem(BaseModel): @@ -45,5 +59,17 @@ class BrowserListResponseItem(BaseModel): proxy_id: Optional[str] = None """ID of the proxy associated with this browser session, if any.""" + viewport: Optional[BrowserListResponseItemViewport] = None + """Initial browser window size in pixels with optional refresh rate. + + If omitted, image defaults apply (commonly 1024x768@60). Only specific viewport + configurations are supported. The server will reject unsupported combinations. + Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1024x768@60 If refresh_rate is not provided, it will be + automatically determined from the width and height if they match a supported + configuration exactly. Note: Higher resolutions may affect the responsiveness of + live view browser + """ + BrowserListResponse: TypeAlias = List[BrowserListResponseItem] diff --git a/src/kernel/types/browser_upload_extensions_params.py b/src/kernel/types/browser_load_extensions_params.py similarity index 84% rename from src/kernel/types/browser_upload_extensions_params.py rename to src/kernel/types/browser_load_extensions_params.py index ab0363cb..6212380c 100644 --- a/src/kernel/types/browser_upload_extensions_params.py +++ b/src/kernel/types/browser_load_extensions_params.py @@ -7,10 +7,10 @@ from .._types import FileTypes -__all__ = ["BrowserUploadExtensionsParams", "Extension"] +__all__ = ["BrowserLoadExtensionsParams", "Extension"] -class BrowserUploadExtensionsParams(TypedDict, total=False): +class BrowserLoadExtensionsParams(TypedDict, total=False): extensions: Required[Iterable[Extension]] """List of extensions to upload and activate""" diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 29ce4c17..2da39afa 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -7,7 +7,21 @@ from .._models import BaseModel from .browser_persistence import BrowserPersistence -__all__ = ["BrowserRetrieveResponse"] +__all__ = ["BrowserRetrieveResponse", "Viewport"] + + +class Viewport(BaseModel): + height: int + """Browser window height in pixels.""" + + width: int + """Browser window width in pixels.""" + + refresh_rate: Optional[int] = None + """Display refresh rate in Hz. + + If omitted, automatically determined from width and height. + """ class BrowserRetrieveResponse(BaseModel): @@ -43,3 +57,15 @@ class BrowserRetrieveResponse(BaseModel): proxy_id: Optional[str] = None """ID of the proxy associated with this browser session, if any.""" + + viewport: Optional[Viewport] = None + """Initial browser window size in pixels with optional refresh rate. + + If omitted, image defaults apply (commonly 1024x768@60). Only specific viewport + configurations are supported. The server will reject unsupported combinations. + Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1024x768@60 If refresh_rate is not provided, it will be + automatically determined from the width and height if they match a supported + configuration exactly. Note: Higher resolutions may affect the responsiveness of + live view browser + """ diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index c3e7a7fe..e8ee60a4 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -48,6 +48,11 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: proxy_id="proxy_id", stealth=True, timeout_seconds=10, + viewport={ + "height": 800, + "width": 1280, + "refresh_rate": 60, + }, ) assert_matches_type(BrowserCreateResponse, browser, path=["response"]) @@ -221,8 +226,8 @@ def test_path_params_delete_by_id(self, client: Kernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_upload_extensions(self, client: Kernel) -> None: - browser = client.browsers.upload_extensions( + def test_method_load_extensions(self, client: Kernel) -> None: + browser = client.browsers.load_extensions( id="id", extensions=[ { @@ -235,8 +240,8 @@ def test_method_upload_extensions(self, client: Kernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_raw_response_upload_extensions(self, client: Kernel) -> None: - response = client.browsers.with_raw_response.upload_extensions( + def test_raw_response_load_extensions(self, client: Kernel) -> None: + response = client.browsers.with_raw_response.load_extensions( id="id", extensions=[ { @@ -253,8 +258,8 @@ def test_raw_response_upload_extensions(self, client: Kernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_streaming_response_upload_extensions(self, client: Kernel) -> None: - with client.browsers.with_streaming_response.upload_extensions( + def test_streaming_response_load_extensions(self, client: Kernel) -> None: + with client.browsers.with_streaming_response.load_extensions( id="id", extensions=[ { @@ -273,9 +278,9 @@ def test_streaming_response_upload_extensions(self, client: Kernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_path_params_upload_extensions(self, client: Kernel) -> None: + def test_path_params_load_extensions(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - client.browsers.with_raw_response.upload_extensions( + client.browsers.with_raw_response.load_extensions( id="", extensions=[ { @@ -318,6 +323,11 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> proxy_id="proxy_id", stealth=True, timeout_seconds=10, + viewport={ + "height": 800, + "width": 1280, + "refresh_rate": 60, + }, ) assert_matches_type(BrowserCreateResponse, browser, path=["response"]) @@ -491,8 +501,8 @@ async def test_path_params_delete_by_id(self, async_client: AsyncKernel) -> None @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_upload_extensions(self, async_client: AsyncKernel) -> None: - browser = await async_client.browsers.upload_extensions( + async def test_method_load_extensions(self, async_client: AsyncKernel) -> None: + browser = await async_client.browsers.load_extensions( id="id", extensions=[ { @@ -505,8 +515,8 @@ async def test_method_upload_extensions(self, async_client: AsyncKernel) -> None @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_raw_response_upload_extensions(self, async_client: AsyncKernel) -> None: - response = await async_client.browsers.with_raw_response.upload_extensions( + async def test_raw_response_load_extensions(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.with_raw_response.load_extensions( id="id", extensions=[ { @@ -523,8 +533,8 @@ async def test_raw_response_upload_extensions(self, async_client: AsyncKernel) - @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_streaming_response_upload_extensions(self, async_client: AsyncKernel) -> None: - async with async_client.browsers.with_streaming_response.upload_extensions( + async def test_streaming_response_load_extensions(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.with_streaming_response.load_extensions( id="id", extensions=[ { @@ -543,9 +553,9 @@ async def test_streaming_response_upload_extensions(self, async_client: AsyncKer @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_path_params_upload_extensions(self, async_client: AsyncKernel) -> None: + async def test_path_params_load_extensions(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - await async_client.browsers.with_raw_response.upload_extensions( + await async_client.browsers.with_raw_response.load_extensions( id="", extensions=[ { From 6bebadfb6ae734dbf9c27d6cc693f50a525f9531 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 15:48:01 +0000 Subject: [PATCH 189/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index a26ebfc1..54c4d98a 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.14.0" + ".": "0.14.1" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index b06cd8f7..f3f78745 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.14.0" +version = "0.14.1" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 0bebaf17..365119e9 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.14.0" # x-release-please-version +__version__ = "0.14.1" # x-release-please-version From 96c09671ab2fe48525736abf73c80c0d4b720644 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 17:12:20 +0000 Subject: [PATCH 190/448] feat: Kiosk mode --- .stats.yml | 4 ++-- src/kernel/resources/browsers/browsers.py | 10 ++++++++++ src/kernel/types/browser_create_params.py | 6 ++++++ src/kernel/types/browser_create_response.py | 3 +++ src/kernel/types/browser_list_response.py | 3 +++ src/kernel/types/browser_retrieve_response.py | 3 +++ tests/api_resources/test_browsers.py | 2 ++ 7 files changed, 29 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 99609550..6bb4af83 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 57 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-1cd328ccf61f0e888d6df27b091c30b38c392ab9ca8ce7fd0ead8f10aaf71ffa.yml -openapi_spec_hash: af761c48d1955f11822f3b95f9c46750 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-6c765f1c4ce1c4dd4ceb371f56bf047aa79af36031ba43cbd68fa16a5fdb9bb3.yml +openapi_spec_hash: e9086f69281360f4e0895c9274a59531 config_hash: deadfc4d2b0a947673bcf559b5db6e1b diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 3e3eb8df..1d444214 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -100,6 +100,7 @@ def create( extensions: Iterable[browser_create_params.Extension] | Omit = omit, headless: bool | Omit = omit, invocation_id: str | Omit = omit, + kiosk_mode: bool | Omit = omit, persistence: BrowserPersistenceParam | Omit = omit, profile: browser_create_params.Profile | Omit = omit, proxy_id: str | Omit = omit, @@ -124,6 +125,9 @@ def create( invocation_id: action invocation ID + kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live + view. + persistence: Optional persistence configuration for the browser session. profile: Profile selection for the browser session. Provide either id or name. If @@ -167,6 +171,7 @@ def create( "extensions": extensions, "headless": headless, "invocation_id": invocation_id, + "kiosk_mode": kiosk_mode, "persistence": persistence, "profile": profile, "proxy_id": proxy_id, @@ -395,6 +400,7 @@ async def create( extensions: Iterable[browser_create_params.Extension] | Omit = omit, headless: bool | Omit = omit, invocation_id: str | Omit = omit, + kiosk_mode: bool | Omit = omit, persistence: BrowserPersistenceParam | Omit = omit, profile: browser_create_params.Profile | Omit = omit, proxy_id: str | Omit = omit, @@ -419,6 +425,9 @@ async def create( invocation_id: action invocation ID + kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live + view. + persistence: Optional persistence configuration for the browser session. profile: Profile selection for the browser session. Provide either id or name. If @@ -462,6 +471,7 @@ async def create( "extensions": extensions, "headless": headless, "invocation_id": invocation_id, + "kiosk_mode": kiosk_mode, "persistence": persistence, "profile": profile, "proxy_id": proxy_id, diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index a0214a51..23c3bb81 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -26,6 +26,12 @@ class BrowserCreateParams(TypedDict, total=False): invocation_id: str """action invocation ID""" + kiosk_mode: bool + """ + If true, launches the browser in kiosk mode to hide address bar and tabs in live + view. + """ + persistence: BrowserPersistenceParam """Optional persistence configuration for the browser session.""" diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index d7ef603c..bcc50450 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -49,6 +49,9 @@ class BrowserCreateResponse(BaseModel): Only available for non-headless browsers. """ + kiosk_mode: Optional[bool] = None + """Whether the browser session is running in kiosk mode.""" + persistence: Optional[BrowserPersistence] = None """Optional persistence configuration for the browser session.""" diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index 22d72e18..a1b332fe 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -50,6 +50,9 @@ class BrowserListResponseItem(BaseModel): Only available for non-headless browsers. """ + kiosk_mode: Optional[bool] = None + """Whether the browser session is running in kiosk mode.""" + persistence: Optional[BrowserPersistence] = None """Optional persistence configuration for the browser session.""" diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 2da39afa..f233929c 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -49,6 +49,9 @@ class BrowserRetrieveResponse(BaseModel): Only available for non-headless browsers. """ + kiosk_mode: Optional[bool] = None + """Whether the browser session is running in kiosk mode.""" + persistence: Optional[BrowserPersistence] = None """Optional persistence configuration for the browser session.""" diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index e8ee60a4..bd75630e 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -39,6 +39,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: ], headless=False, invocation_id="rr33xuugxj9h0bkf1rdt2bet", + kiosk_mode=True, persistence={"id": "my-awesome-browser-for-user-1234"}, profile={ "id": "id", @@ -314,6 +315,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> ], headless=False, invocation_id="rr33xuugxj9h0bkf1rdt2bet", + kiosk_mode=True, persistence={"id": "my-awesome-browser-for-user-1234"}, profile={ "id": "id", From 04ad6b2fcaa71c94ded2f6e0e4a21c8242267c06 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 17:29:25 +0000 Subject: [PATCH 191/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 54c4d98a..3d26904f 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.14.1" + ".": "0.14.2" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index f3f78745..c2e14c5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.14.1" +version = "0.14.2" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 365119e9..dfcce591 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.14.1" # x-release-please-version +__version__ = "0.14.2" # x-release-please-version From fe46b34253d0a23b2f595b2bd81fd0de76ee11d1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 20:31:16 +0000 Subject: [PATCH 192/448] feat: Phani/deploy with GitHub url --- .stats.yml | 6 +- README.md | 1 - api.md | 6 +- src/kernel/resources/deployments.py | 28 +- src/kernel/resources/extensions.py | 304 ++++++++-------- src/kernel/types/__init__.py | 4 +- src/kernel/types/deployment_create_params.py | 41 ++- ...d_params.py => extension_create_params.py} | 4 +- ...sponse.py => extension_create_response.py} | 4 +- tests/api_resources/test_deployments.py | 56 +-- tests/api_resources/test_extensions.py | 324 +++++++++--------- 11 files changed, 410 insertions(+), 368 deletions(-) rename src/kernel/types/{extension_upload_params.py => extension_create_params.py} (81%) rename src/kernel/types/{extension_upload_response.py => extension_create_response.py} (88%) diff --git a/.stats.yml b/.stats.yml index 6bb4af83..2bd40ccf 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 57 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-6c765f1c4ce1c4dd4ceb371f56bf047aa79af36031ba43cbd68fa16a5fdb9bb3.yml -openapi_spec_hash: e9086f69281360f4e0895c9274a59531 -config_hash: deadfc4d2b0a947673bcf559b5db6e1b +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-6eaa6f5654abc94549962d7db1e8c7936af1f815bb3abe2f8249959394da1278.yml +openapi_spec_hash: 31ece7cd801e74228b80a8112a762e56 +config_hash: 3fc2057ce765bc5f27785a694ed0f553 diff --git a/README.md b/README.md index beec3f01..ae0066ec 100644 --- a/README.md +++ b/README.md @@ -218,7 +218,6 @@ from kernel import Kernel client = Kernel() client.deployments.create( - entrypoint_rel_path="src/app.py", file=Path("/path/to/file"), ) ``` diff --git a/api.md b/api.md index 6059a78d..9f219359 100644 --- a/api.md +++ b/api.md @@ -202,13 +202,13 @@ Methods: Types: ```python -from kernel.types import ExtensionListResponse, ExtensionUploadResponse +from kernel.types import ExtensionCreateResponse, ExtensionListResponse ``` Methods: +- client.extensions.create(\*\*params) -> ExtensionCreateResponse +- client.extensions.retrieve(id_or_name) -> BinaryAPIResponse - client.extensions.list() -> ExtensionListResponse - client.extensions.delete(id_or_name) -> None -- client.extensions.download(id_or_name) -> BinaryAPIResponse - client.extensions.download_from_chrome_store(\*\*params) -> BinaryAPIResponse -- client.extensions.upload(\*\*params) -> ExtensionUploadResponse diff --git a/src/kernel/resources/deployments.py b/src/kernel/resources/deployments.py index 15812440..bdc200f1 100644 --- a/src/kernel/resources/deployments.py +++ b/src/kernel/resources/deployments.py @@ -52,11 +52,12 @@ def with_streaming_response(self) -> DeploymentsResourceWithStreamingResponse: def create( self, *, - entrypoint_rel_path: str, - file: FileTypes, + entrypoint_rel_path: str | Omit = omit, env_vars: Dict[str, str] | Omit = omit, + file: FileTypes | Omit = omit, force: bool | Omit = omit, region: Literal["aws.us-east-1a"] | Omit = omit, + source: deployment_create_params.Source | Omit = omit, version: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -71,15 +72,17 @@ def create( Args: entrypoint_rel_path: Relative path to the entrypoint of the application - file: ZIP file containing the application source directory - env_vars: Map of environment variables to set for the deployed application. Each key-value pair represents an environment variable. + file: ZIP file containing the application source directory + force: Allow overwriting an existing app version region: Region for deployment. Currently we only support "aws.us-east-1a" + source: Source from which to fetch application code. + version: Version of the application. Can be any string. extra_headers: Send extra headers @@ -93,10 +96,11 @@ def create( body = deepcopy_minimal( { "entrypoint_rel_path": entrypoint_rel_path, - "file": file, "env_vars": env_vars, + "file": file, "force": force, "region": region, + "source": source, "version": version, } ) @@ -271,11 +275,12 @@ def with_streaming_response(self) -> AsyncDeploymentsResourceWithStreamingRespon async def create( self, *, - entrypoint_rel_path: str, - file: FileTypes, + entrypoint_rel_path: str | Omit = omit, env_vars: Dict[str, str] | Omit = omit, + file: FileTypes | Omit = omit, force: bool | Omit = omit, region: Literal["aws.us-east-1a"] | Omit = omit, + source: deployment_create_params.Source | Omit = omit, version: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -290,15 +295,17 @@ async def create( Args: entrypoint_rel_path: Relative path to the entrypoint of the application - file: ZIP file containing the application source directory - env_vars: Map of environment variables to set for the deployed application. Each key-value pair represents an environment variable. + file: ZIP file containing the application source directory + force: Allow overwriting an existing app version region: Region for deployment. Currently we only support "aws.us-east-1a" + source: Source from which to fetch application code. + version: Version of the application. Can be any string. extra_headers: Send extra headers @@ -312,10 +319,11 @@ async def create( body = deepcopy_minimal( { "entrypoint_rel_path": entrypoint_rel_path, - "file": file, "env_vars": env_vars, + "file": file, "force": force, "region": region, + "source": source, "version": version, } ) diff --git a/src/kernel/resources/extensions.py b/src/kernel/resources/extensions.py index 2f868716..45d08d91 100644 --- a/src/kernel/resources/extensions.py +++ b/src/kernel/resources/extensions.py @@ -7,7 +7,7 @@ import httpx -from ..types import extension_upload_params, extension_download_from_chrome_store_params +from ..types import extension_create_params, extension_download_from_chrome_store_params from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, FileTypes, omit, not_given from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform from .._compat import cached_property @@ -28,7 +28,7 @@ ) from .._base_client import make_request_options from ..types.extension_list_response import ExtensionListResponse -from ..types.extension_upload_response import ExtensionUploadResponse +from ..types.extension_create_response import ExtensionCreateResponse __all__ = ["ExtensionsResource", "AsyncExtensionsResource"] @@ -53,26 +53,58 @@ def with_streaming_response(self) -> ExtensionsResourceWithStreamingResponse: """ return ExtensionsResourceWithStreamingResponse(self) - def list( + def create( self, *, + file: FileTypes, + name: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ExtensionListResponse: - """List extensions owned by the caller's organization.""" - return self._get( + ) -> ExtensionCreateResponse: + """Upload a zip file containing an unpacked browser extension. + + Optionally provide a + unique name for later reference. + + Args: + file: ZIP file containing the browser extension. + + name: Optional unique name within the organization to reference this extension. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "file": file, + "name": name, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return self._post( "/extensions", + body=maybe_transform(body, extension_create_params.ExtensionCreateParams), + files=files, options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ExtensionListResponse, + cast_to=ExtensionCreateResponse, ) - def delete( + def retrieve( self, id_or_name: str, *, @@ -82,9 +114,9 @@ def delete( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> None: + ) -> BinaryAPIResponse: """ - Delete an extension by its ID or by its name. + Download the extension as a ZIP archive by ID or name. Args: extra_headers: Send extra headers @@ -97,16 +129,35 @@ def delete( """ if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return self._delete( + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + return self._get( f"/extensions/{id_or_name}", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=NoneType, + cast_to=BinaryAPIResponse, + ) + + def list( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ExtensionListResponse: + """List extensions owned by the caller's organization.""" + return self._get( + "/extensions", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ExtensionListResponse, ) - def download( + def delete( self, id_or_name: str, *, @@ -116,9 +167,9 @@ def download( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> BinaryAPIResponse: + ) -> None: """ - Download the extension as a ZIP archive by ID or name. + Delete an extension by its ID or by its name. Args: extra_headers: Send extra headers @@ -131,13 +182,13 @@ def download( """ if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") - extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} - return self._get( + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( f"/extensions/{id_or_name}", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=BinaryAPIResponse, + cast_to=NoneType, ) def download_from_chrome_store( @@ -188,7 +239,28 @@ def download_from_chrome_store( cast_to=BinaryAPIResponse, ) - def upload( + +class AsyncExtensionsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncExtensionsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncExtensionsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncExtensionsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return AsyncExtensionsResourceWithStreamingResponse(self) + + async def create( self, *, file: FileTypes, @@ -199,7 +271,7 @@ def upload( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ExtensionUploadResponse: + ) -> ExtensionCreateResponse: """Upload a zip file containing an unpacked browser extension. Optionally provide a @@ -229,36 +301,49 @@ def upload( # sent to the server will contain a `boundary` parameter, e.g. # multipart/form-data; boundary=---abc-- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} - return self._post( + return await self._post( "/extensions", - body=maybe_transform(body, extension_upload_params.ExtensionUploadParams), + body=await async_maybe_transform(body, extension_create_params.ExtensionCreateParams), files=files, options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ExtensionUploadResponse, + cast_to=ExtensionCreateResponse, ) - -class AsyncExtensionsResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncExtensionsResourceWithRawResponse: + async def retrieve( + self, + id_or_name: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncBinaryAPIResponse: """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. + Download the extension as a ZIP archive by ID or name. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers - """ - return AsyncExtensionsResourceWithRawResponse(self) + Args: + extra_headers: Send extra headers - @cached_property - def with_streaming_response(self) -> AsyncExtensionsResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. + extra_query: Add additional query parameters to the request - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds """ - return AsyncExtensionsResourceWithStreamingResponse(self) + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + return await self._get( + f"/extensions/{id_or_name}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AsyncBinaryAPIResponse, + ) async def list( self, @@ -313,40 +398,6 @@ async def delete( cast_to=NoneType, ) - async def download( - self, - id_or_name: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> AsyncBinaryAPIResponse: - """ - Download the extension as a ZIP archive by ID or name. - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not id_or_name: - raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") - extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} - return await self._get( - f"/extensions/{id_or_name}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AsyncBinaryAPIResponse, - ) - async def download_from_chrome_store( self, *, @@ -395,145 +446,94 @@ async def download_from_chrome_store( cast_to=AsyncBinaryAPIResponse, ) - async def upload( - self, - *, - file: FileTypes, - name: str | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ExtensionUploadResponse: - """Upload a zip file containing an unpacked browser extension. - - Optionally provide a - unique name for later reference. - - Args: - file: ZIP file containing the browser extension. - - name: Optional unique name within the organization to reference this extension. - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - body = deepcopy_minimal( - { - "file": file, - "name": name, - } - ) - files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) - # It should be noted that the actual Content-Type header that will be - # sent to the server will contain a `boundary` parameter, e.g. - # multipart/form-data; boundary=---abc-- - extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} - return await self._post( - "/extensions", - body=await async_maybe_transform(body, extension_upload_params.ExtensionUploadParams), - files=files, - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ExtensionUploadResponse, - ) - class ExtensionsResourceWithRawResponse: def __init__(self, extensions: ExtensionsResource) -> None: self._extensions = extensions + self.create = to_raw_response_wrapper( + extensions.create, + ) + self.retrieve = to_custom_raw_response_wrapper( + extensions.retrieve, + BinaryAPIResponse, + ) self.list = to_raw_response_wrapper( extensions.list, ) self.delete = to_raw_response_wrapper( extensions.delete, ) - self.download = to_custom_raw_response_wrapper( - extensions.download, - BinaryAPIResponse, - ) self.download_from_chrome_store = to_custom_raw_response_wrapper( extensions.download_from_chrome_store, BinaryAPIResponse, ) - self.upload = to_raw_response_wrapper( - extensions.upload, - ) class AsyncExtensionsResourceWithRawResponse: def __init__(self, extensions: AsyncExtensionsResource) -> None: self._extensions = extensions + self.create = async_to_raw_response_wrapper( + extensions.create, + ) + self.retrieve = async_to_custom_raw_response_wrapper( + extensions.retrieve, + AsyncBinaryAPIResponse, + ) self.list = async_to_raw_response_wrapper( extensions.list, ) self.delete = async_to_raw_response_wrapper( extensions.delete, ) - self.download = async_to_custom_raw_response_wrapper( - extensions.download, - AsyncBinaryAPIResponse, - ) self.download_from_chrome_store = async_to_custom_raw_response_wrapper( extensions.download_from_chrome_store, AsyncBinaryAPIResponse, ) - self.upload = async_to_raw_response_wrapper( - extensions.upload, - ) class ExtensionsResourceWithStreamingResponse: def __init__(self, extensions: ExtensionsResource) -> None: self._extensions = extensions + self.create = to_streamed_response_wrapper( + extensions.create, + ) + self.retrieve = to_custom_streamed_response_wrapper( + extensions.retrieve, + StreamedBinaryAPIResponse, + ) self.list = to_streamed_response_wrapper( extensions.list, ) self.delete = to_streamed_response_wrapper( extensions.delete, ) - self.download = to_custom_streamed_response_wrapper( - extensions.download, - StreamedBinaryAPIResponse, - ) self.download_from_chrome_store = to_custom_streamed_response_wrapper( extensions.download_from_chrome_store, StreamedBinaryAPIResponse, ) - self.upload = to_streamed_response_wrapper( - extensions.upload, - ) class AsyncExtensionsResourceWithStreamingResponse: def __init__(self, extensions: AsyncExtensionsResource) -> None: self._extensions = extensions + self.create = async_to_streamed_response_wrapper( + extensions.create, + ) + self.retrieve = async_to_custom_streamed_response_wrapper( + extensions.retrieve, + AsyncStreamedBinaryAPIResponse, + ) self.list = async_to_streamed_response_wrapper( extensions.list, ) self.delete = async_to_streamed_response_wrapper( extensions.delete, ) - self.download = async_to_custom_streamed_response_wrapper( - extensions.download, - AsyncStreamedBinaryAPIResponse, - ) self.download_from_chrome_store = async_to_custom_streamed_response_wrapper( extensions.download_from_chrome_store, AsyncStreamedBinaryAPIResponse, ) - self.upload = async_to_streamed_response_wrapper( - extensions.upload, - ) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 6b49cf7f..2edc0bcd 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -27,8 +27,8 @@ from .invocation_list_params import InvocationListParams as InvocationListParams from .invocation_state_event import InvocationStateEvent as InvocationStateEvent from .browser_create_response import BrowserCreateResponse as BrowserCreateResponse +from .extension_create_params import ExtensionCreateParams as ExtensionCreateParams from .extension_list_response import ExtensionListResponse as ExtensionListResponse -from .extension_upload_params import ExtensionUploadParams as ExtensionUploadParams from .proxy_retrieve_response import ProxyRetrieveResponse as ProxyRetrieveResponse from .deployment_create_params import DeploymentCreateParams as DeploymentCreateParams from .deployment_follow_params import DeploymentFollowParams as DeploymentFollowParams @@ -39,7 +39,7 @@ from .invocation_update_params import InvocationUpdateParams as InvocationUpdateParams from .browser_persistence_param import BrowserPersistenceParam as BrowserPersistenceParam from .browser_retrieve_response import BrowserRetrieveResponse as BrowserRetrieveResponse -from .extension_upload_response import ExtensionUploadResponse as ExtensionUploadResponse +from .extension_create_response import ExtensionCreateResponse as ExtensionCreateResponse from .deployment_create_response import DeploymentCreateResponse as DeploymentCreateResponse from .deployment_follow_response import DeploymentFollowResponse as DeploymentFollowResponse from .invocation_create_response import InvocationCreateResponse as InvocationCreateResponse diff --git a/src/kernel/types/deployment_create_params.py b/src/kernel/types/deployment_create_params.py index 6701c0a8..16eb5702 100644 --- a/src/kernel/types/deployment_create_params.py +++ b/src/kernel/types/deployment_create_params.py @@ -7,27 +7,58 @@ from .._types import FileTypes -__all__ = ["DeploymentCreateParams"] +__all__ = ["DeploymentCreateParams", "Source", "SourceAuth"] class DeploymentCreateParams(TypedDict, total=False): - entrypoint_rel_path: Required[str] + entrypoint_rel_path: str """Relative path to the entrypoint of the application""" - file: Required[FileTypes] - """ZIP file containing the application source directory""" - env_vars: Dict[str, str] """Map of environment variables to set for the deployed application. Each key-value pair represents an environment variable. """ + file: FileTypes + """ZIP file containing the application source directory""" + force: bool """Allow overwriting an existing app version""" region: Literal["aws.us-east-1a"] """Region for deployment. Currently we only support "aws.us-east-1a" """ + source: Source + """Source from which to fetch application code.""" + version: str """Version of the application. Can be any string.""" + + +class SourceAuth(TypedDict, total=False): + token: Required[str] + """GitHub PAT or installation access token""" + + method: Required[Literal["github_token"]] + """Auth method""" + + +class Source(TypedDict, total=False): + entrypoint: Required[str] + """Relative path to the application entrypoint within the selected path.""" + + ref: Required[str] + """Git ref (branch, tag, or commit SHA) to fetch.""" + + type: Required[Literal["github"]] + """Source type identifier.""" + + url: Required[str] + """Base repository URL (without blob/tree suffixes).""" + + auth: SourceAuth + """Authentication for private repositories.""" + + path: str + """Path within the repo to deploy (omit to use repo root).""" diff --git a/src/kernel/types/extension_upload_params.py b/src/kernel/types/extension_create_params.py similarity index 81% rename from src/kernel/types/extension_upload_params.py rename to src/kernel/types/extension_create_params.py index d36dde31..6bb2b397 100644 --- a/src/kernel/types/extension_upload_params.py +++ b/src/kernel/types/extension_create_params.py @@ -6,10 +6,10 @@ from .._types import FileTypes -__all__ = ["ExtensionUploadParams"] +__all__ = ["ExtensionCreateParams"] -class ExtensionUploadParams(TypedDict, total=False): +class ExtensionCreateParams(TypedDict, total=False): file: Required[FileTypes] """ZIP file containing the browser extension.""" diff --git a/src/kernel/types/extension_upload_response.py b/src/kernel/types/extension_create_response.py similarity index 88% rename from src/kernel/types/extension_upload_response.py rename to src/kernel/types/extension_create_response.py index 373e8861..c4fd6301 100644 --- a/src/kernel/types/extension_upload_response.py +++ b/src/kernel/types/extension_create_response.py @@ -5,10 +5,10 @@ from .._models import BaseModel -__all__ = ["ExtensionUploadResponse"] +__all__ = ["ExtensionCreateResponse"] -class ExtensionUploadResponse(BaseModel): +class ExtensionCreateResponse(BaseModel): id: str """Unique identifier for the extension""" diff --git a/tests/api_resources/test_deployments.py b/tests/api_resources/test_deployments.py index fc5d2991..6c3354ef 100644 --- a/tests/api_resources/test_deployments.py +++ b/tests/api_resources/test_deployments.py @@ -25,10 +25,7 @@ class TestDeployments: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_create(self, client: Kernel) -> None: - deployment = client.deployments.create( - entrypoint_rel_path="src/app.py", - file=b"raw file contents", - ) + deployment = client.deployments.create() assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @@ -36,10 +33,21 @@ def test_method_create(self, client: Kernel) -> None: def test_method_create_with_all_params(self, client: Kernel) -> None: deployment = client.deployments.create( entrypoint_rel_path="src/app.py", + env_vars={"FOO": "bar"}, file=b"raw file contents", - env_vars={"foo": "string"}, force=False, region="aws.us-east-1a", + source={ + "entrypoint": "src/index.ts", + "ref": "main", + "type": "github", + "url": "https://github.com/org/repo", + "auth": { + "token": "ghs_***", + "method": "github_token", + }, + "path": "apps/api", + }, version="1.0.0", ) assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) @@ -47,10 +55,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_create(self, client: Kernel) -> None: - response = client.deployments.with_raw_response.create( - entrypoint_rel_path="src/app.py", - file=b"raw file contents", - ) + response = client.deployments.with_raw_response.create() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -60,10 +65,7 @@ def test_raw_response_create(self, client: Kernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_create(self, client: Kernel) -> None: - with client.deployments.with_streaming_response.create( - entrypoint_rel_path="src/app.py", - file=b"raw file contents", - ) as response: + with client.deployments.with_streaming_response.create() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -211,10 +213,7 @@ class TestAsyncDeployments: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_create(self, async_client: AsyncKernel) -> None: - deployment = await async_client.deployments.create( - entrypoint_rel_path="src/app.py", - file=b"raw file contents", - ) + deployment = await async_client.deployments.create() assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @@ -222,10 +221,21 @@ async def test_method_create(self, async_client: AsyncKernel) -> None: async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: deployment = await async_client.deployments.create( entrypoint_rel_path="src/app.py", + env_vars={"FOO": "bar"}, file=b"raw file contents", - env_vars={"foo": "string"}, force=False, region="aws.us-east-1a", + source={ + "entrypoint": "src/index.ts", + "ref": "main", + "type": "github", + "url": "https://github.com/org/repo", + "auth": { + "token": "ghs_***", + "method": "github_token", + }, + "path": "apps/api", + }, version="1.0.0", ) assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) @@ -233,10 +243,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_create(self, async_client: AsyncKernel) -> None: - response = await async_client.deployments.with_raw_response.create( - entrypoint_rel_path="src/app.py", - file=b"raw file contents", - ) + response = await async_client.deployments.with_raw_response.create() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -246,10 +253,7 @@ async def test_raw_response_create(self, async_client: AsyncKernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: - async with async_client.deployments.with_streaming_response.create( - entrypoint_rel_path="src/app.py", - file=b"raw file contents", - ) as response: + async with async_client.deployments.with_streaming_response.create() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/api_resources/test_extensions.py b/tests/api_resources/test_extensions.py index 5d61f327..ffdb02f6 100644 --- a/tests/api_resources/test_extensions.py +++ b/tests/api_resources/test_extensions.py @@ -13,7 +13,7 @@ from tests.utils import assert_matches_type from kernel.types import ( ExtensionListResponse, - ExtensionUploadResponse, + ExtensionCreateResponse, ) from kernel._response import ( BinaryAPIResponse, @@ -28,6 +28,99 @@ class TestExtensions: 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: Kernel) -> None: + extension = client.extensions.create( + file=b"raw file contents", + ) + assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_create_with_all_params(self, client: Kernel) -> None: + extension = client.extensions.create( + file=b"raw file contents", + name="name", + ) + assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_create(self, client: Kernel) -> None: + response = client.extensions.with_raw_response.create( + file=b"raw file contents", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + extension = response.parse() + assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_create(self, client: Kernel) -> None: + with client.extensions.with_streaming_response.create( + file=b"raw file contents", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + extension = response.parse() + assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_method_retrieve(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + extension = client.extensions.retrieve( + "id_or_name", + ) + assert extension.is_closed + assert extension.json() == {"foo": "bar"} + assert cast(Any, extension.is_closed) is True + assert isinstance(extension, BinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_raw_response_retrieve(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + extension = client.extensions.with_raw_response.retrieve( + "id_or_name", + ) + + assert extension.is_closed is True + assert extension.http_request.headers.get("X-Stainless-Lang") == "python" + assert extension.json() == {"foo": "bar"} + assert isinstance(extension, BinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_streaming_response_retrieve(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + with client.extensions.with_streaming_response.retrieve( + "id_or_name", + ) as extension: + assert not extension.is_closed + assert extension.http_request.headers.get("X-Stainless-Lang") == "python" + + assert extension.json() == {"foo": "bar"} + assert cast(Any, extension.is_closed) is True + assert isinstance(extension, StreamedBinaryAPIResponse) + + assert cast(Any, extension.is_closed) is True + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_path_params_retrieve(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): + client.extensions.with_raw_response.retrieve( + "", + ) + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_list(self, client: Kernel) -> None: @@ -98,56 +191,6 @@ def test_path_params_delete(self, client: Kernel) -> None: "", ) - @parametrize - @pytest.mark.respx(base_url=base_url) - def test_method_download(self, client: Kernel, respx_mock: MockRouter) -> None: - respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - extension = client.extensions.download( - "id_or_name", - ) - assert extension.is_closed - assert extension.json() == {"foo": "bar"} - assert cast(Any, extension.is_closed) is True - assert isinstance(extension, BinaryAPIResponse) - - @parametrize - @pytest.mark.respx(base_url=base_url) - def test_raw_response_download(self, client: Kernel, respx_mock: MockRouter) -> None: - respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - - extension = client.extensions.with_raw_response.download( - "id_or_name", - ) - - assert extension.is_closed is True - assert extension.http_request.headers.get("X-Stainless-Lang") == "python" - assert extension.json() == {"foo": "bar"} - assert isinstance(extension, BinaryAPIResponse) - - @parametrize - @pytest.mark.respx(base_url=base_url) - def test_streaming_response_download(self, client: Kernel, respx_mock: MockRouter) -> None: - respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - with client.extensions.with_streaming_response.download( - "id_or_name", - ) as extension: - assert not extension.is_closed - assert extension.http_request.headers.get("X-Stainless-Lang") == "python" - - assert extension.json() == {"foo": "bar"} - assert cast(Any, extension.is_closed) is True - assert isinstance(extension, StreamedBinaryAPIResponse) - - assert cast(Any, extension.is_closed) is True - - @parametrize - @pytest.mark.respx(base_url=base_url) - def test_path_params_download(self, client: Kernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): - client.extensions.with_raw_response.download( - "", - ) - @parametrize @pytest.mark.respx(base_url=base_url) def test_method_download_from_chrome_store(self, client: Kernel, respx_mock: MockRouter) -> None: @@ -203,54 +246,104 @@ def test_streaming_response_download_from_chrome_store(self, client: Kernel, res assert cast(Any, extension.is_closed) is True + +class TestAsyncExtensions: + 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 - def test_method_upload(self, client: Kernel) -> None: - extension = client.extensions.upload( + async def test_method_create(self, async_client: AsyncKernel) -> None: + extension = await async_client.extensions.create( file=b"raw file contents", ) - assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) + assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_upload_with_all_params(self, client: Kernel) -> None: - extension = client.extensions.upload( + async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: + extension = await async_client.extensions.create( file=b"raw file contents", name="name", ) - assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) + assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_raw_response_upload(self, client: Kernel) -> None: - response = client.extensions.with_raw_response.upload( + async def test_raw_response_create(self, async_client: AsyncKernel) -> None: + response = await async_client.extensions.with_raw_response.create( file=b"raw file contents", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" - extension = response.parse() - assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) + extension = await response.parse() + assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_streaming_response_upload(self, client: Kernel) -> None: - with client.extensions.with_streaming_response.upload( + async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: + async with async_client.extensions.with_streaming_response.create( file=b"raw file contents", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" - extension = response.parse() - assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) + extension = await response.parse() + assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) assert cast(Any, response.is_closed) is True + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_method_retrieve(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + extension = await async_client.extensions.retrieve( + "id_or_name", + ) + assert extension.is_closed + assert await extension.json() == {"foo": "bar"} + assert cast(Any, extension.is_closed) is True + assert isinstance(extension, AsyncBinaryAPIResponse) -class TestAsyncExtensions: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_raw_response_retrieve(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + extension = await async_client.extensions.with_raw_response.retrieve( + "id_or_name", + ) + + assert extension.is_closed is True + assert extension.http_request.headers.get("X-Stainless-Lang") == "python" + assert await extension.json() == {"foo": "bar"} + assert isinstance(extension, AsyncBinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_streaming_response_retrieve(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + async with async_client.extensions.with_streaming_response.retrieve( + "id_or_name", + ) as extension: + assert not extension.is_closed + assert extension.http_request.headers.get("X-Stainless-Lang") == "python" + + assert await extension.json() == {"foo": "bar"} + assert cast(Any, extension.is_closed) is True + assert isinstance(extension, AsyncStreamedBinaryAPIResponse) + + assert cast(Any, extension.is_closed) is True + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): + await async_client.extensions.with_raw_response.retrieve( + "", + ) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize @@ -322,56 +415,6 @@ async def test_path_params_delete(self, async_client: AsyncKernel) -> None: "", ) - @parametrize - @pytest.mark.respx(base_url=base_url) - async def test_method_download(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: - respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - extension = await async_client.extensions.download( - "id_or_name", - ) - assert extension.is_closed - assert await extension.json() == {"foo": "bar"} - assert cast(Any, extension.is_closed) is True - assert isinstance(extension, AsyncBinaryAPIResponse) - - @parametrize - @pytest.mark.respx(base_url=base_url) - async def test_raw_response_download(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: - respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - - extension = await async_client.extensions.with_raw_response.download( - "id_or_name", - ) - - assert extension.is_closed is True - assert extension.http_request.headers.get("X-Stainless-Lang") == "python" - assert await extension.json() == {"foo": "bar"} - assert isinstance(extension, AsyncBinaryAPIResponse) - - @parametrize - @pytest.mark.respx(base_url=base_url) - async def test_streaming_response_download(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: - respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - async with async_client.extensions.with_streaming_response.download( - "id_or_name", - ) as extension: - assert not extension.is_closed - assert extension.http_request.headers.get("X-Stainless-Lang") == "python" - - assert await extension.json() == {"foo": "bar"} - assert cast(Any, extension.is_closed) is True - assert isinstance(extension, AsyncStreamedBinaryAPIResponse) - - assert cast(Any, extension.is_closed) is True - - @parametrize - @pytest.mark.respx(base_url=base_url) - async def test_path_params_download(self, async_client: AsyncKernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): - await async_client.extensions.with_raw_response.download( - "", - ) - @parametrize @pytest.mark.respx(base_url=base_url) async def test_method_download_from_chrome_store(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: @@ -432,46 +475,3 @@ async def test_streaming_response_download_from_chrome_store( assert isinstance(extension, AsyncStreamedBinaryAPIResponse) assert cast(Any, extension.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_upload(self, async_client: AsyncKernel) -> None: - extension = await async_client.extensions.upload( - file=b"raw file contents", - ) - assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_upload_with_all_params(self, async_client: AsyncKernel) -> None: - extension = await async_client.extensions.upload( - file=b"raw file contents", - name="name", - ) - assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_upload(self, async_client: AsyncKernel) -> None: - response = await async_client.extensions.with_raw_response.upload( - file=b"raw file contents", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - extension = await response.parse() - assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_upload(self, async_client: AsyncKernel) -> None: - async with async_client.extensions.with_streaming_response.upload( - file=b"raw file contents", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - extension = await response.parse() - assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) - - assert cast(Any, response.is_closed) is True From 8a6632e56a8afc460e17b08f4dbb74f317d156ae Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 18:33:29 +0000 Subject: [PATCH 193/448] feat: click mouse, move mouse, screenshot --- .stats.yml | 8 +- api.md | 18 +- src/kernel/resources/browsers/__init__.py | 14 + src/kernel/resources/browsers/browsers.py | 32 + src/kernel/resources/browsers/computer.py | 949 ++++++++++++++++++ src/kernel/resources/extensions.py | 304 +++--- src/kernel/types/__init__.py | 4 +- src/kernel/types/browsers/__init__.py | 7 + .../computer_capture_screenshot_params.py | 25 + .../browsers/computer_click_mouse_params.py | 29 + .../browsers/computer_drag_mouse_params.py | 36 + .../browsers/computer_move_mouse_params.py | 20 + .../browsers/computer_press_key_params.py | 28 + .../types/browsers/computer_scroll_params.py | 26 + .../browsers/computer_type_text_params.py | 15 + ...e_params.py => extension_upload_params.py} | 4 +- ...sponse.py => extension_upload_response.py} | 4 +- tests/api_resources/browsers/test_computer.py | 892 ++++++++++++++++ tests/api_resources/test_extensions.py | 324 +++--- 19 files changed, 2412 insertions(+), 327 deletions(-) create mode 100644 src/kernel/resources/browsers/computer.py create mode 100644 src/kernel/types/browsers/computer_capture_screenshot_params.py create mode 100644 src/kernel/types/browsers/computer_click_mouse_params.py create mode 100644 src/kernel/types/browsers/computer_drag_mouse_params.py create mode 100644 src/kernel/types/browsers/computer_move_mouse_params.py create mode 100644 src/kernel/types/browsers/computer_press_key_params.py create mode 100644 src/kernel/types/browsers/computer_scroll_params.py create mode 100644 src/kernel/types/browsers/computer_type_text_params.py rename src/kernel/types/{extension_create_params.py => extension_upload_params.py} (81%) rename src/kernel/types/{extension_create_response.py => extension_upload_response.py} (88%) create mode 100644 tests/api_resources/browsers/test_computer.py diff --git a/.stats.yml b/.stats.yml index 2bd40ccf..b4dc6064 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 57 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-6eaa6f5654abc94549962d7db1e8c7936af1f815bb3abe2f8249959394da1278.yml -openapi_spec_hash: 31ece7cd801e74228b80a8112a762e56 -config_hash: 3fc2057ce765bc5f27785a694ed0f553 +configured_endpoints: 64 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-e21f0324774a1762bc2bba0da3a8a6b0d0e74720d7a1c83dec813f9e027fcf58.yml +openapi_spec_hash: f1b636abfd6cb8e7c2ba7ffb8e53b9ba +config_hash: 09a2df23048cb16689c9a390d9e5bc47 diff --git a/api.md b/api.md index 9f219359..858dbfd8 100644 --- a/api.md +++ b/api.md @@ -166,6 +166,18 @@ Methods: - client.browsers.logs.stream(id, \*\*params) -> LogEvent +## Computer + +Methods: + +- client.browsers.computer.capture_screenshot(id, \*\*params) -> BinaryAPIResponse +- client.browsers.computer.click_mouse(id, \*\*params) -> None +- client.browsers.computer.drag_mouse(id, \*\*params) -> None +- client.browsers.computer.move_mouse(id, \*\*params) -> None +- client.browsers.computer.press_key(id, \*\*params) -> None +- client.browsers.computer.scroll(id, \*\*params) -> None +- client.browsers.computer.type_text(id, \*\*params) -> None + # Profiles Types: @@ -202,13 +214,13 @@ Methods: Types: ```python -from kernel.types import ExtensionCreateResponse, ExtensionListResponse +from kernel.types import ExtensionListResponse, ExtensionUploadResponse ``` Methods: -- client.extensions.create(\*\*params) -> ExtensionCreateResponse -- client.extensions.retrieve(id_or_name) -> BinaryAPIResponse - client.extensions.list() -> ExtensionListResponse - client.extensions.delete(id_or_name) -> None +- client.extensions.download(id_or_name) -> BinaryAPIResponse - client.extensions.download_from_chrome_store(\*\*params) -> BinaryAPIResponse +- client.extensions.upload(\*\*params) -> ExtensionUploadResponse diff --git a/src/kernel/resources/browsers/__init__.py b/src/kernel/resources/browsers/__init__.py index 97c987e4..abcc8f78 100644 --- a/src/kernel/resources/browsers/__init__.py +++ b/src/kernel/resources/browsers/__init__.py @@ -40,6 +40,14 @@ BrowsersResourceWithStreamingResponse, AsyncBrowsersResourceWithStreamingResponse, ) +from .computer import ( + ComputerResource, + AsyncComputerResource, + ComputerResourceWithRawResponse, + AsyncComputerResourceWithRawResponse, + ComputerResourceWithStreamingResponse, + AsyncComputerResourceWithStreamingResponse, +) __all__ = [ "ReplaysResource", @@ -66,6 +74,12 @@ "AsyncLogsResourceWithRawResponse", "LogsResourceWithStreamingResponse", "AsyncLogsResourceWithStreamingResponse", + "ComputerResource", + "AsyncComputerResource", + "ComputerResourceWithRawResponse", + "AsyncComputerResourceWithRawResponse", + "ComputerResourceWithStreamingResponse", + "AsyncComputerResourceWithStreamingResponse", "BrowsersResource", "AsyncBrowsersResource", "BrowsersResourceWithRawResponse", diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 1d444214..c65a738a 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -41,6 +41,14 @@ ) from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform +from .computer import ( + ComputerResource, + AsyncComputerResource, + ComputerResourceWithRawResponse, + AsyncComputerResourceWithRawResponse, + ComputerResourceWithStreamingResponse, + AsyncComputerResourceWithStreamingResponse, +) from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -75,6 +83,10 @@ def process(self) -> ProcessResource: def logs(self) -> LogsResource: return LogsResource(self._client) + @cached_property + def computer(self) -> ComputerResource: + return ComputerResource(self._client) + @cached_property def with_raw_response(self) -> BrowsersResourceWithRawResponse: """ @@ -375,6 +387,10 @@ def process(self) -> AsyncProcessResource: def logs(self) -> AsyncLogsResource: return AsyncLogsResource(self._client) + @cached_property + def computer(self) -> AsyncComputerResource: + return AsyncComputerResource(self._client) + @cached_property def with_raw_response(self) -> AsyncBrowsersResourceWithRawResponse: """ @@ -699,6 +715,10 @@ def process(self) -> ProcessResourceWithRawResponse: def logs(self) -> LogsResourceWithRawResponse: return LogsResourceWithRawResponse(self._browsers.logs) + @cached_property + def computer(self) -> ComputerResourceWithRawResponse: + return ComputerResourceWithRawResponse(self._browsers.computer) + class AsyncBrowsersResourceWithRawResponse: def __init__(self, browsers: AsyncBrowsersResource) -> None: @@ -739,6 +759,10 @@ def process(self) -> AsyncProcessResourceWithRawResponse: def logs(self) -> AsyncLogsResourceWithRawResponse: return AsyncLogsResourceWithRawResponse(self._browsers.logs) + @cached_property + def computer(self) -> AsyncComputerResourceWithRawResponse: + return AsyncComputerResourceWithRawResponse(self._browsers.computer) + class BrowsersResourceWithStreamingResponse: def __init__(self, browsers: BrowsersResource) -> None: @@ -779,6 +803,10 @@ def process(self) -> ProcessResourceWithStreamingResponse: def logs(self) -> LogsResourceWithStreamingResponse: return LogsResourceWithStreamingResponse(self._browsers.logs) + @cached_property + def computer(self) -> ComputerResourceWithStreamingResponse: + return ComputerResourceWithStreamingResponse(self._browsers.computer) + class AsyncBrowsersResourceWithStreamingResponse: def __init__(self, browsers: AsyncBrowsersResource) -> None: @@ -818,3 +846,7 @@ def process(self) -> AsyncProcessResourceWithStreamingResponse: @cached_property def logs(self) -> AsyncLogsResourceWithStreamingResponse: return AsyncLogsResourceWithStreamingResponse(self._browsers.logs) + + @cached_property + def computer(self) -> AsyncComputerResourceWithStreamingResponse: + return AsyncComputerResourceWithStreamingResponse(self._browsers.computer) diff --git a/src/kernel/resources/browsers/computer.py b/src/kernel/resources/browsers/computer.py new file mode 100644 index 00000000..68cee420 --- /dev/null +++ b/src/kernel/resources/browsers/computer.py @@ -0,0 +1,949 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Iterable +from typing_extensions import Literal + +import httpx + +from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, SequenceNotStr, omit, not_given +from ..._utils import maybe_transform, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + to_custom_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_custom_streamed_response_wrapper, + async_to_custom_raw_response_wrapper, + async_to_custom_streamed_response_wrapper, +) +from ..._base_client import make_request_options +from ...types.browsers import ( + computer_scroll_params, + computer_press_key_params, + computer_type_text_params, + computer_drag_mouse_params, + computer_move_mouse_params, + computer_click_mouse_params, + computer_capture_screenshot_params, +) + +__all__ = ["ComputerResource", "AsyncComputerResource"] + + +class ComputerResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> ComputerResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return ComputerResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ComputerResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return ComputerResourceWithStreamingResponse(self) + + def capture_screenshot( + self, + id: str, + *, + region: computer_capture_screenshot_params.Region | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> BinaryAPIResponse: + """ + Capture a screenshot of the browser instance + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "image/png", **(extra_headers or {})} + return self._post( + f"/browsers/{id}/computer/screenshot", + body=maybe_transform( + {"region": region}, computer_capture_screenshot_params.ComputerCaptureScreenshotParams + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BinaryAPIResponse, + ) + + def click_mouse( + self, + id: str, + *, + x: int, + y: int, + button: Literal["left", "right", "middle", "back", "forward"] | Omit = omit, + click_type: Literal["down", "up", "click"] | Omit = omit, + hold_keys: SequenceNotStr[str] | Omit = omit, + num_clicks: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + Simulate a mouse click action on the browser instance + + Args: + x: X coordinate of the click position + + y: Y coordinate of the click position + + button: Mouse button to interact with + + click_type: Type of click action + + hold_keys: Modifier keys to hold during the click + + num_clicks: Number of times to repeat the click + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._post( + f"/browsers/{id}/computer/click_mouse", + body=maybe_transform( + { + "x": x, + "y": y, + "button": button, + "click_type": click_type, + "hold_keys": hold_keys, + "num_clicks": num_clicks, + }, + computer_click_mouse_params.ComputerClickMouseParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + def drag_mouse( + self, + id: str, + *, + path: Iterable[Iterable[int]], + button: Literal["left", "middle", "right"] | Omit = omit, + delay: int | Omit = omit, + hold_keys: SequenceNotStr[str] | Omit = omit, + step_delay_ms: int | Omit = omit, + steps_per_segment: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + Drag the mouse along a path + + Args: + path: Ordered list of [x, y] coordinate pairs to move through while dragging. Must + contain at least 2 points. + + button: Mouse button to drag with + + delay: Delay in milliseconds between button down and starting to move along the path. + + hold_keys: Modifier keys to hold during the drag + + step_delay_ms: Delay in milliseconds between relative steps while dragging (not the initial + delay). + + steps_per_segment: Number of relative move steps per segment in the path. Minimum 1. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._post( + f"/browsers/{id}/computer/drag_mouse", + body=maybe_transform( + { + "path": path, + "button": button, + "delay": delay, + "hold_keys": hold_keys, + "step_delay_ms": step_delay_ms, + "steps_per_segment": steps_per_segment, + }, + computer_drag_mouse_params.ComputerDragMouseParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + def move_mouse( + self, + id: str, + *, + x: int, + y: int, + hold_keys: SequenceNotStr[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + Move the mouse cursor to the specified coordinates on the browser instance + + Args: + x: X coordinate to move the cursor to + + y: Y coordinate to move the cursor to + + hold_keys: Modifier keys to hold during the move + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._post( + f"/browsers/{id}/computer/move_mouse", + body=maybe_transform( + { + "x": x, + "y": y, + "hold_keys": hold_keys, + }, + computer_move_mouse_params.ComputerMoveMouseParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + def press_key( + self, + id: str, + *, + keys: SequenceNotStr[str], + duration: int | Omit = omit, + hold_keys: SequenceNotStr[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + Press one or more keys on the host computer + + Args: + keys: List of key symbols to press. Each item should be a key symbol supported by + xdotool (see X11 keysym definitions). Examples include "Return", "Shift", + "Ctrl", "Alt", "F5". Items in this list could also be combinations, e.g. + "Ctrl+t" or "Ctrl+Shift+Tab". + + duration: Duration to hold the keys down in milliseconds. If omitted or 0, keys are + tapped. + + hold_keys: Optional modifier keys to hold during the key press sequence. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._post( + f"/browsers/{id}/computer/press_key", + body=maybe_transform( + { + "keys": keys, + "duration": duration, + "hold_keys": hold_keys, + }, + computer_press_key_params.ComputerPressKeyParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + def scroll( + self, + id: str, + *, + x: int, + y: int, + delta_x: int | Omit = omit, + delta_y: int | Omit = omit, + hold_keys: SequenceNotStr[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + Scroll the mouse wheel at a position on the host computer + + Args: + x: X coordinate at which to perform the scroll + + y: Y coordinate at which to perform the scroll + + delta_x: Horizontal scroll amount. Positive scrolls right, negative scrolls left. + + delta_y: Vertical scroll amount. Positive scrolls down, negative scrolls up. + + hold_keys: Modifier keys to hold during the scroll + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._post( + f"/browsers/{id}/computer/scroll", + body=maybe_transform( + { + "x": x, + "y": y, + "delta_x": delta_x, + "delta_y": delta_y, + "hold_keys": hold_keys, + }, + computer_scroll_params.ComputerScrollParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + def type_text( + self, + id: str, + *, + text: str, + delay: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + Type text on the browser instance + + Args: + text: Text to type on the browser instance + + delay: Delay in milliseconds between keystrokes + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._post( + f"/browsers/{id}/computer/type", + body=maybe_transform( + { + "text": text, + "delay": delay, + }, + computer_type_text_params.ComputerTypeTextParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class AsyncComputerResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncComputerResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncComputerResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncComputerResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return AsyncComputerResourceWithStreamingResponse(self) + + async def capture_screenshot( + self, + id: str, + *, + region: computer_capture_screenshot_params.Region | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncBinaryAPIResponse: + """ + Capture a screenshot of the browser instance + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "image/png", **(extra_headers or {})} + return await self._post( + f"/browsers/{id}/computer/screenshot", + body=await async_maybe_transform( + {"region": region}, computer_capture_screenshot_params.ComputerCaptureScreenshotParams + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AsyncBinaryAPIResponse, + ) + + async def click_mouse( + self, + id: str, + *, + x: int, + y: int, + button: Literal["left", "right", "middle", "back", "forward"] | Omit = omit, + click_type: Literal["down", "up", "click"] | Omit = omit, + hold_keys: SequenceNotStr[str] | Omit = omit, + num_clicks: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + Simulate a mouse click action on the browser instance + + Args: + x: X coordinate of the click position + + y: Y coordinate of the click position + + button: Mouse button to interact with + + click_type: Type of click action + + hold_keys: Modifier keys to hold during the click + + num_clicks: Number of times to repeat the click + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._post( + f"/browsers/{id}/computer/click_mouse", + body=await async_maybe_transform( + { + "x": x, + "y": y, + "button": button, + "click_type": click_type, + "hold_keys": hold_keys, + "num_clicks": num_clicks, + }, + computer_click_mouse_params.ComputerClickMouseParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + async def drag_mouse( + self, + id: str, + *, + path: Iterable[Iterable[int]], + button: Literal["left", "middle", "right"] | Omit = omit, + delay: int | Omit = omit, + hold_keys: SequenceNotStr[str] | Omit = omit, + step_delay_ms: int | Omit = omit, + steps_per_segment: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + Drag the mouse along a path + + Args: + path: Ordered list of [x, y] coordinate pairs to move through while dragging. Must + contain at least 2 points. + + button: Mouse button to drag with + + delay: Delay in milliseconds between button down and starting to move along the path. + + hold_keys: Modifier keys to hold during the drag + + step_delay_ms: Delay in milliseconds between relative steps while dragging (not the initial + delay). + + steps_per_segment: Number of relative move steps per segment in the path. Minimum 1. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._post( + f"/browsers/{id}/computer/drag_mouse", + body=await async_maybe_transform( + { + "path": path, + "button": button, + "delay": delay, + "hold_keys": hold_keys, + "step_delay_ms": step_delay_ms, + "steps_per_segment": steps_per_segment, + }, + computer_drag_mouse_params.ComputerDragMouseParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + async def move_mouse( + self, + id: str, + *, + x: int, + y: int, + hold_keys: SequenceNotStr[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + Move the mouse cursor to the specified coordinates on the browser instance + + Args: + x: X coordinate to move the cursor to + + y: Y coordinate to move the cursor to + + hold_keys: Modifier keys to hold during the move + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._post( + f"/browsers/{id}/computer/move_mouse", + body=await async_maybe_transform( + { + "x": x, + "y": y, + "hold_keys": hold_keys, + }, + computer_move_mouse_params.ComputerMoveMouseParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + async def press_key( + self, + id: str, + *, + keys: SequenceNotStr[str], + duration: int | Omit = omit, + hold_keys: SequenceNotStr[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + Press one or more keys on the host computer + + Args: + keys: List of key symbols to press. Each item should be a key symbol supported by + xdotool (see X11 keysym definitions). Examples include "Return", "Shift", + "Ctrl", "Alt", "F5". Items in this list could also be combinations, e.g. + "Ctrl+t" or "Ctrl+Shift+Tab". + + duration: Duration to hold the keys down in milliseconds. If omitted or 0, keys are + tapped. + + hold_keys: Optional modifier keys to hold during the key press sequence. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._post( + f"/browsers/{id}/computer/press_key", + body=await async_maybe_transform( + { + "keys": keys, + "duration": duration, + "hold_keys": hold_keys, + }, + computer_press_key_params.ComputerPressKeyParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + async def scroll( + self, + id: str, + *, + x: int, + y: int, + delta_x: int | Omit = omit, + delta_y: int | Omit = omit, + hold_keys: SequenceNotStr[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + Scroll the mouse wheel at a position on the host computer + + Args: + x: X coordinate at which to perform the scroll + + y: Y coordinate at which to perform the scroll + + delta_x: Horizontal scroll amount. Positive scrolls right, negative scrolls left. + + delta_y: Vertical scroll amount. Positive scrolls down, negative scrolls up. + + hold_keys: Modifier keys to hold during the scroll + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._post( + f"/browsers/{id}/computer/scroll", + body=await async_maybe_transform( + { + "x": x, + "y": y, + "delta_x": delta_x, + "delta_y": delta_y, + "hold_keys": hold_keys, + }, + computer_scroll_params.ComputerScrollParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + async def type_text( + self, + id: str, + *, + text: str, + delay: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + Type text on the browser instance + + Args: + text: Text to type on the browser instance + + delay: Delay in milliseconds between keystrokes + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._post( + f"/browsers/{id}/computer/type", + body=await async_maybe_transform( + { + "text": text, + "delay": delay, + }, + computer_type_text_params.ComputerTypeTextParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class ComputerResourceWithRawResponse: + def __init__(self, computer: ComputerResource) -> None: + self._computer = computer + + self.capture_screenshot = to_custom_raw_response_wrapper( + computer.capture_screenshot, + BinaryAPIResponse, + ) + self.click_mouse = to_raw_response_wrapper( + computer.click_mouse, + ) + self.drag_mouse = to_raw_response_wrapper( + computer.drag_mouse, + ) + self.move_mouse = to_raw_response_wrapper( + computer.move_mouse, + ) + self.press_key = to_raw_response_wrapper( + computer.press_key, + ) + self.scroll = to_raw_response_wrapper( + computer.scroll, + ) + self.type_text = to_raw_response_wrapper( + computer.type_text, + ) + + +class AsyncComputerResourceWithRawResponse: + def __init__(self, computer: AsyncComputerResource) -> None: + self._computer = computer + + self.capture_screenshot = async_to_custom_raw_response_wrapper( + computer.capture_screenshot, + AsyncBinaryAPIResponse, + ) + self.click_mouse = async_to_raw_response_wrapper( + computer.click_mouse, + ) + self.drag_mouse = async_to_raw_response_wrapper( + computer.drag_mouse, + ) + self.move_mouse = async_to_raw_response_wrapper( + computer.move_mouse, + ) + self.press_key = async_to_raw_response_wrapper( + computer.press_key, + ) + self.scroll = async_to_raw_response_wrapper( + computer.scroll, + ) + self.type_text = async_to_raw_response_wrapper( + computer.type_text, + ) + + +class ComputerResourceWithStreamingResponse: + def __init__(self, computer: ComputerResource) -> None: + self._computer = computer + + self.capture_screenshot = to_custom_streamed_response_wrapper( + computer.capture_screenshot, + StreamedBinaryAPIResponse, + ) + self.click_mouse = to_streamed_response_wrapper( + computer.click_mouse, + ) + self.drag_mouse = to_streamed_response_wrapper( + computer.drag_mouse, + ) + self.move_mouse = to_streamed_response_wrapper( + computer.move_mouse, + ) + self.press_key = to_streamed_response_wrapper( + computer.press_key, + ) + self.scroll = to_streamed_response_wrapper( + computer.scroll, + ) + self.type_text = to_streamed_response_wrapper( + computer.type_text, + ) + + +class AsyncComputerResourceWithStreamingResponse: + def __init__(self, computer: AsyncComputerResource) -> None: + self._computer = computer + + self.capture_screenshot = async_to_custom_streamed_response_wrapper( + computer.capture_screenshot, + AsyncStreamedBinaryAPIResponse, + ) + self.click_mouse = async_to_streamed_response_wrapper( + computer.click_mouse, + ) + self.drag_mouse = async_to_streamed_response_wrapper( + computer.drag_mouse, + ) + self.move_mouse = async_to_streamed_response_wrapper( + computer.move_mouse, + ) + self.press_key = async_to_streamed_response_wrapper( + computer.press_key, + ) + self.scroll = async_to_streamed_response_wrapper( + computer.scroll, + ) + self.type_text = async_to_streamed_response_wrapper( + computer.type_text, + ) diff --git a/src/kernel/resources/extensions.py b/src/kernel/resources/extensions.py index 45d08d91..2f868716 100644 --- a/src/kernel/resources/extensions.py +++ b/src/kernel/resources/extensions.py @@ -7,7 +7,7 @@ import httpx -from ..types import extension_create_params, extension_download_from_chrome_store_params +from ..types import extension_upload_params, extension_download_from_chrome_store_params from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, FileTypes, omit, not_given from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform from .._compat import cached_property @@ -28,7 +28,7 @@ ) from .._base_client import make_request_options from ..types.extension_list_response import ExtensionListResponse -from ..types.extension_create_response import ExtensionCreateResponse +from ..types.extension_upload_response import ExtensionUploadResponse __all__ = ["ExtensionsResource", "AsyncExtensionsResource"] @@ -53,58 +53,26 @@ def with_streaming_response(self) -> ExtensionsResourceWithStreamingResponse: """ return ExtensionsResourceWithStreamingResponse(self) - def create( + def list( self, *, - file: FileTypes, - name: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ExtensionCreateResponse: - """Upload a zip file containing an unpacked browser extension. - - Optionally provide a - unique name for later reference. - - Args: - file: ZIP file containing the browser extension. - - name: Optional unique name within the organization to reference this extension. - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - body = deepcopy_minimal( - { - "file": file, - "name": name, - } - ) - files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) - # It should be noted that the actual Content-Type header that will be - # sent to the server will contain a `boundary` parameter, e.g. - # multipart/form-data; boundary=---abc-- - extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} - return self._post( + ) -> ExtensionListResponse: + """List extensions owned by the caller's organization.""" + return self._get( "/extensions", - body=maybe_transform(body, extension_create_params.ExtensionCreateParams), - files=files, options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ExtensionCreateResponse, + cast_to=ExtensionListResponse, ) - def retrieve( + def delete( self, id_or_name: str, *, @@ -114,9 +82,9 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> BinaryAPIResponse: + ) -> None: """ - Download the extension as a ZIP archive by ID or name. + Delete an extension by its ID or by its name. Args: extra_headers: Send extra headers @@ -129,35 +97,16 @@ def retrieve( """ if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") - extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} - return self._get( + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( f"/extensions/{id_or_name}", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=BinaryAPIResponse, - ) - - def list( - self, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ExtensionListResponse: - """List extensions owned by the caller's organization.""" - return self._get( - "/extensions", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ExtensionListResponse, + cast_to=NoneType, ) - def delete( + def download( self, id_or_name: str, *, @@ -167,9 +116,9 @@ def delete( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> None: + ) -> BinaryAPIResponse: """ - Delete an extension by its ID or by its name. + Download the extension as a ZIP archive by ID or name. Args: extra_headers: Send extra headers @@ -182,13 +131,13 @@ def delete( """ if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return self._delete( + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + return self._get( f"/extensions/{id_or_name}", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=NoneType, + cast_to=BinaryAPIResponse, ) def download_from_chrome_store( @@ -239,28 +188,7 @@ def download_from_chrome_store( cast_to=BinaryAPIResponse, ) - -class AsyncExtensionsResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncExtensionsResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers - """ - return AsyncExtensionsResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncExtensionsResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response - """ - return AsyncExtensionsResourceWithStreamingResponse(self) - - async def create( + def upload( self, *, file: FileTypes, @@ -271,7 +199,7 @@ async def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ExtensionCreateResponse: + ) -> ExtensionUploadResponse: """Upload a zip file containing an unpacked browser extension. Optionally provide a @@ -301,49 +229,36 @@ async def create( # sent to the server will contain a `boundary` parameter, e.g. # multipart/form-data; boundary=---abc-- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} - return await self._post( + return self._post( "/extensions", - body=await async_maybe_transform(body, extension_create_params.ExtensionCreateParams), + body=maybe_transform(body, extension_upload_params.ExtensionUploadParams), files=files, options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ExtensionCreateResponse, + cast_to=ExtensionUploadResponse, ) - async def retrieve( - self, - id_or_name: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> AsyncBinaryAPIResponse: - """ - Download the extension as a ZIP archive by ID or name. - Args: - extra_headers: Send extra headers +class AsyncExtensionsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncExtensionsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. - extra_query: Add additional query parameters to the request + For more information, see https://www.github.com/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncExtensionsResourceWithRawResponse(self) - extra_body: Add additional JSON properties to the request + @cached_property + def with_streaming_response(self) -> AsyncExtensionsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. - timeout: Override the client-level default timeout for this request, in seconds + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response """ - if not id_or_name: - raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") - extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} - return await self._get( - f"/extensions/{id_or_name}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AsyncBinaryAPIResponse, - ) + return AsyncExtensionsResourceWithStreamingResponse(self) async def list( self, @@ -398,6 +313,40 @@ async def delete( cast_to=NoneType, ) + async def download( + self, + id_or_name: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncBinaryAPIResponse: + """ + Download the extension as a ZIP archive by ID or name. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + return await self._get( + f"/extensions/{id_or_name}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AsyncBinaryAPIResponse, + ) + async def download_from_chrome_store( self, *, @@ -446,94 +395,145 @@ async def download_from_chrome_store( cast_to=AsyncBinaryAPIResponse, ) + async def upload( + self, + *, + file: FileTypes, + name: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ExtensionUploadResponse: + """Upload a zip file containing an unpacked browser extension. + + Optionally provide a + unique name for later reference. + + Args: + file: ZIP file containing the browser extension. + + name: Optional unique name within the organization to reference this extension. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "file": file, + "name": name, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return await self._post( + "/extensions", + body=await async_maybe_transform(body, extension_upload_params.ExtensionUploadParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ExtensionUploadResponse, + ) + class ExtensionsResourceWithRawResponse: def __init__(self, extensions: ExtensionsResource) -> None: self._extensions = extensions - self.create = to_raw_response_wrapper( - extensions.create, - ) - self.retrieve = to_custom_raw_response_wrapper( - extensions.retrieve, - BinaryAPIResponse, - ) self.list = to_raw_response_wrapper( extensions.list, ) self.delete = to_raw_response_wrapper( extensions.delete, ) + self.download = to_custom_raw_response_wrapper( + extensions.download, + BinaryAPIResponse, + ) self.download_from_chrome_store = to_custom_raw_response_wrapper( extensions.download_from_chrome_store, BinaryAPIResponse, ) + self.upload = to_raw_response_wrapper( + extensions.upload, + ) class AsyncExtensionsResourceWithRawResponse: def __init__(self, extensions: AsyncExtensionsResource) -> None: self._extensions = extensions - self.create = async_to_raw_response_wrapper( - extensions.create, - ) - self.retrieve = async_to_custom_raw_response_wrapper( - extensions.retrieve, - AsyncBinaryAPIResponse, - ) self.list = async_to_raw_response_wrapper( extensions.list, ) self.delete = async_to_raw_response_wrapper( extensions.delete, ) + self.download = async_to_custom_raw_response_wrapper( + extensions.download, + AsyncBinaryAPIResponse, + ) self.download_from_chrome_store = async_to_custom_raw_response_wrapper( extensions.download_from_chrome_store, AsyncBinaryAPIResponse, ) + self.upload = async_to_raw_response_wrapper( + extensions.upload, + ) class ExtensionsResourceWithStreamingResponse: def __init__(self, extensions: ExtensionsResource) -> None: self._extensions = extensions - self.create = to_streamed_response_wrapper( - extensions.create, - ) - self.retrieve = to_custom_streamed_response_wrapper( - extensions.retrieve, - StreamedBinaryAPIResponse, - ) self.list = to_streamed_response_wrapper( extensions.list, ) self.delete = to_streamed_response_wrapper( extensions.delete, ) + self.download = to_custom_streamed_response_wrapper( + extensions.download, + StreamedBinaryAPIResponse, + ) self.download_from_chrome_store = to_custom_streamed_response_wrapper( extensions.download_from_chrome_store, StreamedBinaryAPIResponse, ) + self.upload = to_streamed_response_wrapper( + extensions.upload, + ) class AsyncExtensionsResourceWithStreamingResponse: def __init__(self, extensions: AsyncExtensionsResource) -> None: self._extensions = extensions - self.create = async_to_streamed_response_wrapper( - extensions.create, - ) - self.retrieve = async_to_custom_streamed_response_wrapper( - extensions.retrieve, - AsyncStreamedBinaryAPIResponse, - ) self.list = async_to_streamed_response_wrapper( extensions.list, ) self.delete = async_to_streamed_response_wrapper( extensions.delete, ) + self.download = async_to_custom_streamed_response_wrapper( + extensions.download, + AsyncStreamedBinaryAPIResponse, + ) self.download_from_chrome_store = async_to_custom_streamed_response_wrapper( extensions.download_from_chrome_store, AsyncStreamedBinaryAPIResponse, ) + self.upload = async_to_streamed_response_wrapper( + extensions.upload, + ) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 2edc0bcd..6b49cf7f 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -27,8 +27,8 @@ from .invocation_list_params import InvocationListParams as InvocationListParams from .invocation_state_event import InvocationStateEvent as InvocationStateEvent from .browser_create_response import BrowserCreateResponse as BrowserCreateResponse -from .extension_create_params import ExtensionCreateParams as ExtensionCreateParams from .extension_list_response import ExtensionListResponse as ExtensionListResponse +from .extension_upload_params import ExtensionUploadParams as ExtensionUploadParams from .proxy_retrieve_response import ProxyRetrieveResponse as ProxyRetrieveResponse from .deployment_create_params import DeploymentCreateParams as DeploymentCreateParams from .deployment_follow_params import DeploymentFollowParams as DeploymentFollowParams @@ -39,7 +39,7 @@ from .invocation_update_params import InvocationUpdateParams as InvocationUpdateParams from .browser_persistence_param import BrowserPersistenceParam as BrowserPersistenceParam from .browser_retrieve_response import BrowserRetrieveResponse as BrowserRetrieveResponse -from .extension_create_response import ExtensionCreateResponse as ExtensionCreateResponse +from .extension_upload_response import ExtensionUploadResponse as ExtensionUploadResponse from .deployment_create_response import DeploymentCreateResponse as DeploymentCreateResponse from .deployment_follow_response import DeploymentFollowResponse as DeploymentFollowResponse from .invocation_create_response import InvocationCreateResponse as InvocationCreateResponse diff --git a/src/kernel/types/browsers/__init__.py b/src/kernel/types/browsers/__init__.py index d0b6b383..9b0ed53a 100644 --- a/src/kernel/types/browsers/__init__.py +++ b/src/kernel/types/browsers/__init__.py @@ -22,11 +22,18 @@ from .process_exec_response import ProcessExecResponse as ProcessExecResponse from .process_kill_response import ProcessKillResponse as ProcessKillResponse from .replay_start_response import ReplayStartResponse as ReplayStartResponse +from .computer_scroll_params import ComputerScrollParams as ComputerScrollParams from .process_spawn_response import ProcessSpawnResponse as ProcessSpawnResponse from .process_stdin_response import ProcessStdinResponse as ProcessStdinResponse from .process_status_response import ProcessStatusResponse as ProcessStatusResponse +from .computer_press_key_params import ComputerPressKeyParams as ComputerPressKeyParams +from .computer_type_text_params import ComputerTypeTextParams as ComputerTypeTextParams from .f_create_directory_params import FCreateDirectoryParams as FCreateDirectoryParams from .f_delete_directory_params import FDeleteDirectoryParams as FDeleteDirectoryParams from .f_download_dir_zip_params import FDownloadDirZipParams as FDownloadDirZipParams +from .computer_drag_mouse_params import ComputerDragMouseParams as ComputerDragMouseParams +from .computer_move_mouse_params import ComputerMoveMouseParams as ComputerMoveMouseParams +from .computer_click_mouse_params import ComputerClickMouseParams as ComputerClickMouseParams from .f_set_file_permissions_params import FSetFilePermissionsParams as FSetFilePermissionsParams from .process_stdout_stream_response import ProcessStdoutStreamResponse as ProcessStdoutStreamResponse +from .computer_capture_screenshot_params import ComputerCaptureScreenshotParams as ComputerCaptureScreenshotParams diff --git a/src/kernel/types/browsers/computer_capture_screenshot_params.py b/src/kernel/types/browsers/computer_capture_screenshot_params.py new file mode 100644 index 00000000..942cef30 --- /dev/null +++ b/src/kernel/types/browsers/computer_capture_screenshot_params.py @@ -0,0 +1,25 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["ComputerCaptureScreenshotParams", "Region"] + + +class ComputerCaptureScreenshotParams(TypedDict, total=False): + region: Region + + +class Region(TypedDict, total=False): + height: Required[int] + """Height of the region in pixels""" + + width: Required[int] + """Width of the region in pixels""" + + x: Required[int] + """X coordinate of the region's top-left corner""" + + y: Required[int] + """Y coordinate of the region's top-left corner""" diff --git a/src/kernel/types/browsers/computer_click_mouse_params.py b/src/kernel/types/browsers/computer_click_mouse_params.py new file mode 100644 index 00000000..9bde2e6a --- /dev/null +++ b/src/kernel/types/browsers/computer_click_mouse_params.py @@ -0,0 +1,29 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, TypedDict + +from ..._types import SequenceNotStr + +__all__ = ["ComputerClickMouseParams"] + + +class ComputerClickMouseParams(TypedDict, total=False): + x: Required[int] + """X coordinate of the click position""" + + y: Required[int] + """Y coordinate of the click position""" + + button: Literal["left", "right", "middle", "back", "forward"] + """Mouse button to interact with""" + + click_type: Literal["down", "up", "click"] + """Type of click action""" + + hold_keys: SequenceNotStr[str] + """Modifier keys to hold during the click""" + + num_clicks: int + """Number of times to repeat the click""" diff --git a/src/kernel/types/browsers/computer_drag_mouse_params.py b/src/kernel/types/browsers/computer_drag_mouse_params.py new file mode 100644 index 00000000..fb03b4be --- /dev/null +++ b/src/kernel/types/browsers/computer_drag_mouse_params.py @@ -0,0 +1,36 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Iterable +from typing_extensions import Literal, Required, TypedDict + +from ..._types import SequenceNotStr + +__all__ = ["ComputerDragMouseParams"] + + +class ComputerDragMouseParams(TypedDict, total=False): + path: Required[Iterable[Iterable[int]]] + """Ordered list of [x, y] coordinate pairs to move through while dragging. + + Must contain at least 2 points. + """ + + button: Literal["left", "middle", "right"] + """Mouse button to drag with""" + + delay: int + """Delay in milliseconds between button down and starting to move along the path.""" + + hold_keys: SequenceNotStr[str] + """Modifier keys to hold during the drag""" + + step_delay_ms: int + """ + Delay in milliseconds between relative steps while dragging (not the initial + delay). + """ + + steps_per_segment: int + """Number of relative move steps per segment in the path. Minimum 1.""" diff --git a/src/kernel/types/browsers/computer_move_mouse_params.py b/src/kernel/types/browsers/computer_move_mouse_params.py new file mode 100644 index 00000000..1769e074 --- /dev/null +++ b/src/kernel/types/browsers/computer_move_mouse_params.py @@ -0,0 +1,20 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +from ..._types import SequenceNotStr + +__all__ = ["ComputerMoveMouseParams"] + + +class ComputerMoveMouseParams(TypedDict, total=False): + x: Required[int] + """X coordinate to move the cursor to""" + + y: Required[int] + """Y coordinate to move the cursor to""" + + hold_keys: SequenceNotStr[str] + """Modifier keys to hold during the move""" diff --git a/src/kernel/types/browsers/computer_press_key_params.py b/src/kernel/types/browsers/computer_press_key_params.py new file mode 100644 index 00000000..ea2c9b45 --- /dev/null +++ b/src/kernel/types/browsers/computer_press_key_params.py @@ -0,0 +1,28 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +from ..._types import SequenceNotStr + +__all__ = ["ComputerPressKeyParams"] + + +class ComputerPressKeyParams(TypedDict, total=False): + keys: Required[SequenceNotStr[str]] + """List of key symbols to press. + + Each item should be a key symbol supported by xdotool (see X11 keysym + definitions). Examples include "Return", "Shift", "Ctrl", "Alt", "F5". Items in + this list could also be combinations, e.g. "Ctrl+t" or "Ctrl+Shift+Tab". + """ + + duration: int + """Duration to hold the keys down in milliseconds. + + If omitted or 0, keys are tapped. + """ + + hold_keys: SequenceNotStr[str] + """Optional modifier keys to hold during the key press sequence.""" diff --git a/src/kernel/types/browsers/computer_scroll_params.py b/src/kernel/types/browsers/computer_scroll_params.py new file mode 100644 index 00000000..110cb302 --- /dev/null +++ b/src/kernel/types/browsers/computer_scroll_params.py @@ -0,0 +1,26 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +from ..._types import SequenceNotStr + +__all__ = ["ComputerScrollParams"] + + +class ComputerScrollParams(TypedDict, total=False): + x: Required[int] + """X coordinate at which to perform the scroll""" + + y: Required[int] + """Y coordinate at which to perform the scroll""" + + delta_x: int + """Horizontal scroll amount. Positive scrolls right, negative scrolls left.""" + + delta_y: int + """Vertical scroll amount. Positive scrolls down, negative scrolls up.""" + + hold_keys: SequenceNotStr[str] + """Modifier keys to hold during the scroll""" diff --git a/src/kernel/types/browsers/computer_type_text_params.py b/src/kernel/types/browsers/computer_type_text_params.py new file mode 100644 index 00000000..3a2c5133 --- /dev/null +++ b/src/kernel/types/browsers/computer_type_text_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["ComputerTypeTextParams"] + + +class ComputerTypeTextParams(TypedDict, total=False): + text: Required[str] + """Text to type on the browser instance""" + + delay: int + """Delay in milliseconds between keystrokes""" diff --git a/src/kernel/types/extension_create_params.py b/src/kernel/types/extension_upload_params.py similarity index 81% rename from src/kernel/types/extension_create_params.py rename to src/kernel/types/extension_upload_params.py index 6bb2b397..d36dde31 100644 --- a/src/kernel/types/extension_create_params.py +++ b/src/kernel/types/extension_upload_params.py @@ -6,10 +6,10 @@ from .._types import FileTypes -__all__ = ["ExtensionCreateParams"] +__all__ = ["ExtensionUploadParams"] -class ExtensionCreateParams(TypedDict, total=False): +class ExtensionUploadParams(TypedDict, total=False): file: Required[FileTypes] """ZIP file containing the browser extension.""" diff --git a/src/kernel/types/extension_create_response.py b/src/kernel/types/extension_upload_response.py similarity index 88% rename from src/kernel/types/extension_create_response.py rename to src/kernel/types/extension_upload_response.py index c4fd6301..373e8861 100644 --- a/src/kernel/types/extension_create_response.py +++ b/src/kernel/types/extension_upload_response.py @@ -5,10 +5,10 @@ from .._models import BaseModel -__all__ = ["ExtensionCreateResponse"] +__all__ = ["ExtensionUploadResponse"] -class ExtensionCreateResponse(BaseModel): +class ExtensionUploadResponse(BaseModel): id: str """Unique identifier for the extension""" diff --git a/tests/api_resources/browsers/test_computer.py b/tests/api_resources/browsers/test_computer.py new file mode 100644 index 00000000..9e245481 --- /dev/null +++ b/tests/api_resources/browsers/test_computer.py @@ -0,0 +1,892 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import httpx +import pytest +from respx import MockRouter + +from kernel import Kernel, AsyncKernel +from kernel._response import ( + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestComputer: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_method_capture_screenshot(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.post("/browsers/id/computer/screenshot").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + computer = client.browsers.computer.capture_screenshot( + id="id", + ) + assert computer.is_closed + assert computer.json() == {"foo": "bar"} + assert cast(Any, computer.is_closed) is True + assert isinstance(computer, BinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_method_capture_screenshot_with_all_params(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.post("/browsers/id/computer/screenshot").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + computer = client.browsers.computer.capture_screenshot( + id="id", + region={ + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + ) + assert computer.is_closed + assert computer.json() == {"foo": "bar"} + assert cast(Any, computer.is_closed) is True + assert isinstance(computer, BinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_raw_response_capture_screenshot(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.post("/browsers/id/computer/screenshot").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + computer = client.browsers.computer.with_raw_response.capture_screenshot( + id="id", + ) + + assert computer.is_closed is True + assert computer.http_request.headers.get("X-Stainless-Lang") == "python" + assert computer.json() == {"foo": "bar"} + assert isinstance(computer, BinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_streaming_response_capture_screenshot(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.post("/browsers/id/computer/screenshot").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + with client.browsers.computer.with_streaming_response.capture_screenshot( + id="id", + ) as computer: + assert not computer.is_closed + assert computer.http_request.headers.get("X-Stainless-Lang") == "python" + + assert computer.json() == {"foo": "bar"} + assert cast(Any, computer.is_closed) is True + assert isinstance(computer, StreamedBinaryAPIResponse) + + assert cast(Any, computer.is_closed) is True + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_path_params_capture_screenshot(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.computer.with_raw_response.capture_screenshot( + id="", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_click_mouse(self, client: Kernel) -> None: + computer = client.browsers.computer.click_mouse( + id="id", + x=0, + y=0, + ) + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_click_mouse_with_all_params(self, client: Kernel) -> None: + computer = client.browsers.computer.click_mouse( + id="id", + x=0, + y=0, + button="left", + click_type="down", + hold_keys=["string"], + num_clicks=0, + ) + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_click_mouse(self, client: Kernel) -> None: + response = client.browsers.computer.with_raw_response.click_mouse( + id="id", + x=0, + y=0, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + computer = response.parse() + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_click_mouse(self, client: Kernel) -> None: + with client.browsers.computer.with_streaming_response.click_mouse( + id="id", + x=0, + y=0, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + computer = response.parse() + assert computer is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_click_mouse(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.computer.with_raw_response.click_mouse( + id="", + x=0, + y=0, + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_drag_mouse(self, client: Kernel) -> None: + computer = client.browsers.computer.drag_mouse( + id="id", + path=[[0, 0], [0, 0]], + ) + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_drag_mouse_with_all_params(self, client: Kernel) -> None: + computer = client.browsers.computer.drag_mouse( + id="id", + path=[[0, 0], [0, 0]], + button="left", + delay=0, + hold_keys=["string"], + step_delay_ms=0, + steps_per_segment=1, + ) + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_drag_mouse(self, client: Kernel) -> None: + response = client.browsers.computer.with_raw_response.drag_mouse( + id="id", + path=[[0, 0], [0, 0]], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + computer = response.parse() + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_drag_mouse(self, client: Kernel) -> None: + with client.browsers.computer.with_streaming_response.drag_mouse( + id="id", + path=[[0, 0], [0, 0]], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + computer = response.parse() + assert computer is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_drag_mouse(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.computer.with_raw_response.drag_mouse( + id="", + path=[[0, 0], [0, 0]], + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_move_mouse(self, client: Kernel) -> None: + computer = client.browsers.computer.move_mouse( + id="id", + x=0, + y=0, + ) + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_move_mouse_with_all_params(self, client: Kernel) -> None: + computer = client.browsers.computer.move_mouse( + id="id", + x=0, + y=0, + hold_keys=["string"], + ) + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_move_mouse(self, client: Kernel) -> None: + response = client.browsers.computer.with_raw_response.move_mouse( + id="id", + x=0, + y=0, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + computer = response.parse() + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_move_mouse(self, client: Kernel) -> None: + with client.browsers.computer.with_streaming_response.move_mouse( + id="id", + x=0, + y=0, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + computer = response.parse() + assert computer is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_move_mouse(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.computer.with_raw_response.move_mouse( + id="", + x=0, + y=0, + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_press_key(self, client: Kernel) -> None: + computer = client.browsers.computer.press_key( + id="id", + keys=["string"], + ) + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_press_key_with_all_params(self, client: Kernel) -> None: + computer = client.browsers.computer.press_key( + id="id", + keys=["string"], + duration=0, + hold_keys=["string"], + ) + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_press_key(self, client: Kernel) -> None: + response = client.browsers.computer.with_raw_response.press_key( + id="id", + keys=["string"], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + computer = response.parse() + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_press_key(self, client: Kernel) -> None: + with client.browsers.computer.with_streaming_response.press_key( + id="id", + keys=["string"], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + computer = response.parse() + assert computer is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_press_key(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.computer.with_raw_response.press_key( + id="", + keys=["string"], + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_scroll(self, client: Kernel) -> None: + computer = client.browsers.computer.scroll( + id="id", + x=0, + y=0, + ) + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_scroll_with_all_params(self, client: Kernel) -> None: + computer = client.browsers.computer.scroll( + id="id", + x=0, + y=0, + delta_x=0, + delta_y=0, + hold_keys=["string"], + ) + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_scroll(self, client: Kernel) -> None: + response = client.browsers.computer.with_raw_response.scroll( + id="id", + x=0, + y=0, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + computer = response.parse() + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_scroll(self, client: Kernel) -> None: + with client.browsers.computer.with_streaming_response.scroll( + id="id", + x=0, + y=0, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + computer = response.parse() + assert computer is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_scroll(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.computer.with_raw_response.scroll( + id="", + x=0, + y=0, + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_type_text(self, client: Kernel) -> None: + computer = client.browsers.computer.type_text( + id="id", + text="text", + ) + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_type_text_with_all_params(self, client: Kernel) -> None: + computer = client.browsers.computer.type_text( + id="id", + text="text", + delay=0, + ) + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_type_text(self, client: Kernel) -> None: + response = client.browsers.computer.with_raw_response.type_text( + id="id", + text="text", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + computer = response.parse() + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_type_text(self, client: Kernel) -> None: + with client.browsers.computer.with_streaming_response.type_text( + id="id", + text="text", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + computer = response.parse() + assert computer is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_type_text(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.computer.with_raw_response.type_text( + id="", + text="text", + ) + + +class TestAsyncComputer: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_method_capture_screenshot(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.post("/browsers/id/computer/screenshot").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + computer = await async_client.browsers.computer.capture_screenshot( + id="id", + ) + assert computer.is_closed + assert await computer.json() == {"foo": "bar"} + assert cast(Any, computer.is_closed) is True + assert isinstance(computer, AsyncBinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_method_capture_screenshot_with_all_params( + self, async_client: AsyncKernel, respx_mock: MockRouter + ) -> None: + respx_mock.post("/browsers/id/computer/screenshot").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + computer = await async_client.browsers.computer.capture_screenshot( + id="id", + region={ + "height": 0, + "width": 0, + "x": 0, + "y": 0, + }, + ) + assert computer.is_closed + assert await computer.json() == {"foo": "bar"} + assert cast(Any, computer.is_closed) is True + assert isinstance(computer, AsyncBinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_raw_response_capture_screenshot(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.post("/browsers/id/computer/screenshot").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + computer = await async_client.browsers.computer.with_raw_response.capture_screenshot( + id="id", + ) + + assert computer.is_closed is True + assert computer.http_request.headers.get("X-Stainless-Lang") == "python" + assert await computer.json() == {"foo": "bar"} + assert isinstance(computer, AsyncBinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_streaming_response_capture_screenshot( + self, async_client: AsyncKernel, respx_mock: MockRouter + ) -> None: + respx_mock.post("/browsers/id/computer/screenshot").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + async with async_client.browsers.computer.with_streaming_response.capture_screenshot( + id="id", + ) as computer: + assert not computer.is_closed + assert computer.http_request.headers.get("X-Stainless-Lang") == "python" + + assert await computer.json() == {"foo": "bar"} + assert cast(Any, computer.is_closed) is True + assert isinstance(computer, AsyncStreamedBinaryAPIResponse) + + assert cast(Any, computer.is_closed) is True + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_path_params_capture_screenshot(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.computer.with_raw_response.capture_screenshot( + id="", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_click_mouse(self, async_client: AsyncKernel) -> None: + computer = await async_client.browsers.computer.click_mouse( + id="id", + x=0, + y=0, + ) + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_click_mouse_with_all_params(self, async_client: AsyncKernel) -> None: + computer = await async_client.browsers.computer.click_mouse( + id="id", + x=0, + y=0, + button="left", + click_type="down", + hold_keys=["string"], + num_clicks=0, + ) + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_click_mouse(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.computer.with_raw_response.click_mouse( + id="id", + x=0, + y=0, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + computer = await response.parse() + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_click_mouse(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.computer.with_streaming_response.click_mouse( + id="id", + x=0, + y=0, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + computer = await response.parse() + assert computer is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_click_mouse(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.computer.with_raw_response.click_mouse( + id="", + x=0, + y=0, + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_drag_mouse(self, async_client: AsyncKernel) -> None: + computer = await async_client.browsers.computer.drag_mouse( + id="id", + path=[[0, 0], [0, 0]], + ) + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_drag_mouse_with_all_params(self, async_client: AsyncKernel) -> None: + computer = await async_client.browsers.computer.drag_mouse( + id="id", + path=[[0, 0], [0, 0]], + button="left", + delay=0, + hold_keys=["string"], + step_delay_ms=0, + steps_per_segment=1, + ) + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_drag_mouse(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.computer.with_raw_response.drag_mouse( + id="id", + path=[[0, 0], [0, 0]], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + computer = await response.parse() + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_drag_mouse(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.computer.with_streaming_response.drag_mouse( + id="id", + path=[[0, 0], [0, 0]], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + computer = await response.parse() + assert computer is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_drag_mouse(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.computer.with_raw_response.drag_mouse( + id="", + path=[[0, 0], [0, 0]], + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_move_mouse(self, async_client: AsyncKernel) -> None: + computer = await async_client.browsers.computer.move_mouse( + id="id", + x=0, + y=0, + ) + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_move_mouse_with_all_params(self, async_client: AsyncKernel) -> None: + computer = await async_client.browsers.computer.move_mouse( + id="id", + x=0, + y=0, + hold_keys=["string"], + ) + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_move_mouse(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.computer.with_raw_response.move_mouse( + id="id", + x=0, + y=0, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + computer = await response.parse() + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_move_mouse(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.computer.with_streaming_response.move_mouse( + id="id", + x=0, + y=0, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + computer = await response.parse() + assert computer is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_move_mouse(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.computer.with_raw_response.move_mouse( + id="", + x=0, + y=0, + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_press_key(self, async_client: AsyncKernel) -> None: + computer = await async_client.browsers.computer.press_key( + id="id", + keys=["string"], + ) + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_press_key_with_all_params(self, async_client: AsyncKernel) -> None: + computer = await async_client.browsers.computer.press_key( + id="id", + keys=["string"], + duration=0, + hold_keys=["string"], + ) + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_press_key(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.computer.with_raw_response.press_key( + id="id", + keys=["string"], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + computer = await response.parse() + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_press_key(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.computer.with_streaming_response.press_key( + id="id", + keys=["string"], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + computer = await response.parse() + assert computer is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_press_key(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.computer.with_raw_response.press_key( + id="", + keys=["string"], + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_scroll(self, async_client: AsyncKernel) -> None: + computer = await async_client.browsers.computer.scroll( + id="id", + x=0, + y=0, + ) + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_scroll_with_all_params(self, async_client: AsyncKernel) -> None: + computer = await async_client.browsers.computer.scroll( + id="id", + x=0, + y=0, + delta_x=0, + delta_y=0, + hold_keys=["string"], + ) + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_scroll(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.computer.with_raw_response.scroll( + id="id", + x=0, + y=0, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + computer = await response.parse() + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_scroll(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.computer.with_streaming_response.scroll( + id="id", + x=0, + y=0, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + computer = await response.parse() + assert computer is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_scroll(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.computer.with_raw_response.scroll( + id="", + x=0, + y=0, + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_type_text(self, async_client: AsyncKernel) -> None: + computer = await async_client.browsers.computer.type_text( + id="id", + text="text", + ) + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_type_text_with_all_params(self, async_client: AsyncKernel) -> None: + computer = await async_client.browsers.computer.type_text( + id="id", + text="text", + delay=0, + ) + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_type_text(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.computer.with_raw_response.type_text( + id="id", + text="text", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + computer = await response.parse() + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_type_text(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.computer.with_streaming_response.type_text( + id="id", + text="text", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + computer = await response.parse() + assert computer is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_type_text(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.computer.with_raw_response.type_text( + id="", + text="text", + ) diff --git a/tests/api_resources/test_extensions.py b/tests/api_resources/test_extensions.py index ffdb02f6..5d61f327 100644 --- a/tests/api_resources/test_extensions.py +++ b/tests/api_resources/test_extensions.py @@ -13,7 +13,7 @@ from tests.utils import assert_matches_type from kernel.types import ( ExtensionListResponse, - ExtensionCreateResponse, + ExtensionUploadResponse, ) from kernel._response import ( BinaryAPIResponse, @@ -28,99 +28,6 @@ class TestExtensions: 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: Kernel) -> None: - extension = client.extensions.create( - file=b"raw file contents", - ) - assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_create_with_all_params(self, client: Kernel) -> None: - extension = client.extensions.create( - file=b"raw file contents", - name="name", - ) - assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_create(self, client: Kernel) -> None: - response = client.extensions.with_raw_response.create( - file=b"raw file contents", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - extension = response.parse() - assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_create(self, client: Kernel) -> None: - with client.extensions.with_streaming_response.create( - file=b"raw file contents", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - extension = response.parse() - assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - @pytest.mark.respx(base_url=base_url) - def test_method_retrieve(self, client: Kernel, respx_mock: MockRouter) -> None: - respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - extension = client.extensions.retrieve( - "id_or_name", - ) - assert extension.is_closed - assert extension.json() == {"foo": "bar"} - assert cast(Any, extension.is_closed) is True - assert isinstance(extension, BinaryAPIResponse) - - @parametrize - @pytest.mark.respx(base_url=base_url) - def test_raw_response_retrieve(self, client: Kernel, respx_mock: MockRouter) -> None: - respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - - extension = client.extensions.with_raw_response.retrieve( - "id_or_name", - ) - - assert extension.is_closed is True - assert extension.http_request.headers.get("X-Stainless-Lang") == "python" - assert extension.json() == {"foo": "bar"} - assert isinstance(extension, BinaryAPIResponse) - - @parametrize - @pytest.mark.respx(base_url=base_url) - def test_streaming_response_retrieve(self, client: Kernel, respx_mock: MockRouter) -> None: - respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - with client.extensions.with_streaming_response.retrieve( - "id_or_name", - ) as extension: - assert not extension.is_closed - assert extension.http_request.headers.get("X-Stainless-Lang") == "python" - - assert extension.json() == {"foo": "bar"} - assert cast(Any, extension.is_closed) is True - assert isinstance(extension, StreamedBinaryAPIResponse) - - assert cast(Any, extension.is_closed) is True - - @parametrize - @pytest.mark.respx(base_url=base_url) - def test_path_params_retrieve(self, client: Kernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): - client.extensions.with_raw_response.retrieve( - "", - ) - @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_list(self, client: Kernel) -> None: @@ -191,6 +98,56 @@ def test_path_params_delete(self, client: Kernel) -> None: "", ) + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_method_download(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + extension = client.extensions.download( + "id_or_name", + ) + assert extension.is_closed + assert extension.json() == {"foo": "bar"} + assert cast(Any, extension.is_closed) is True + assert isinstance(extension, BinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_raw_response_download(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + extension = client.extensions.with_raw_response.download( + "id_or_name", + ) + + assert extension.is_closed is True + assert extension.http_request.headers.get("X-Stainless-Lang") == "python" + assert extension.json() == {"foo": "bar"} + assert isinstance(extension, BinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_streaming_response_download(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + with client.extensions.with_streaming_response.download( + "id_or_name", + ) as extension: + assert not extension.is_closed + assert extension.http_request.headers.get("X-Stainless-Lang") == "python" + + assert extension.json() == {"foo": "bar"} + assert cast(Any, extension.is_closed) is True + assert isinstance(extension, StreamedBinaryAPIResponse) + + assert cast(Any, extension.is_closed) is True + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_path_params_download(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): + client.extensions.with_raw_response.download( + "", + ) + @parametrize @pytest.mark.respx(base_url=base_url) def test_method_download_from_chrome_store(self, client: Kernel, respx_mock: MockRouter) -> None: @@ -246,104 +203,54 @@ def test_streaming_response_download_from_chrome_store(self, client: Kernel, res assert cast(Any, extension.is_closed) is True - -class TestAsyncExtensions: - 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: AsyncKernel) -> None: - extension = await async_client.extensions.create( + def test_method_upload(self, client: Kernel) -> None: + extension = client.extensions.upload( file=b"raw file contents", ) - assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) + assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: - extension = await async_client.extensions.create( + def test_method_upload_with_all_params(self, client: Kernel) -> None: + extension = client.extensions.upload( file=b"raw file contents", name="name", ) - assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) + assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_raw_response_create(self, async_client: AsyncKernel) -> None: - response = await async_client.extensions.with_raw_response.create( + def test_raw_response_upload(self, client: Kernel) -> None: + response = client.extensions.with_raw_response.upload( file=b"raw file contents", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" - extension = await response.parse() - assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) + extension = response.parse() + assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: - async with async_client.extensions.with_streaming_response.create( + def test_streaming_response_upload(self, client: Kernel) -> None: + with client.extensions.with_streaming_response.upload( file=b"raw file contents", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" - extension = await response.parse() - assert_matches_type(ExtensionCreateResponse, extension, path=["response"]) + extension = response.parse() + assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) assert cast(Any, response.is_closed) is True - @parametrize - @pytest.mark.respx(base_url=base_url) - async def test_method_retrieve(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: - respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - extension = await async_client.extensions.retrieve( - "id_or_name", - ) - assert extension.is_closed - assert await extension.json() == {"foo": "bar"} - assert cast(Any, extension.is_closed) is True - assert isinstance(extension, AsyncBinaryAPIResponse) - @parametrize - @pytest.mark.respx(base_url=base_url) - async def test_raw_response_retrieve(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: - respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - - extension = await async_client.extensions.with_raw_response.retrieve( - "id_or_name", - ) - - assert extension.is_closed is True - assert extension.http_request.headers.get("X-Stainless-Lang") == "python" - assert await extension.json() == {"foo": "bar"} - assert isinstance(extension, AsyncBinaryAPIResponse) - - @parametrize - @pytest.mark.respx(base_url=base_url) - async def test_streaming_response_retrieve(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: - respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - async with async_client.extensions.with_streaming_response.retrieve( - "id_or_name", - ) as extension: - assert not extension.is_closed - assert extension.http_request.headers.get("X-Stainless-Lang") == "python" - - assert await extension.json() == {"foo": "bar"} - assert cast(Any, extension.is_closed) is True - assert isinstance(extension, AsyncStreamedBinaryAPIResponse) - - assert cast(Any, extension.is_closed) is True - - @parametrize - @pytest.mark.respx(base_url=base_url) - async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): - await async_client.extensions.with_raw_response.retrieve( - "", - ) +class TestAsyncExtensions: + 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 @@ -415,6 +322,56 @@ async def test_path_params_delete(self, async_client: AsyncKernel) -> None: "", ) + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_method_download(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + extension = await async_client.extensions.download( + "id_or_name", + ) + assert extension.is_closed + assert await extension.json() == {"foo": "bar"} + assert cast(Any, extension.is_closed) is True + assert isinstance(extension, AsyncBinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_raw_response_download(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + extension = await async_client.extensions.with_raw_response.download( + "id_or_name", + ) + + assert extension.is_closed is True + assert extension.http_request.headers.get("X-Stainless-Lang") == "python" + assert await extension.json() == {"foo": "bar"} + assert isinstance(extension, AsyncBinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_streaming_response_download(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + async with async_client.extensions.with_streaming_response.download( + "id_or_name", + ) as extension: + assert not extension.is_closed + assert extension.http_request.headers.get("X-Stainless-Lang") == "python" + + assert await extension.json() == {"foo": "bar"} + assert cast(Any, extension.is_closed) is True + assert isinstance(extension, AsyncStreamedBinaryAPIResponse) + + assert cast(Any, extension.is_closed) is True + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_path_params_download(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): + await async_client.extensions.with_raw_response.download( + "", + ) + @parametrize @pytest.mark.respx(base_url=base_url) async def test_method_download_from_chrome_store(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: @@ -475,3 +432,46 @@ async def test_streaming_response_download_from_chrome_store( assert isinstance(extension, AsyncStreamedBinaryAPIResponse) assert cast(Any, extension.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_upload(self, async_client: AsyncKernel) -> None: + extension = await async_client.extensions.upload( + file=b"raw file contents", + ) + assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_upload_with_all_params(self, async_client: AsyncKernel) -> None: + extension = await async_client.extensions.upload( + file=b"raw file contents", + name="name", + ) + assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_upload(self, async_client: AsyncKernel) -> None: + response = await async_client.extensions.with_raw_response.upload( + file=b"raw file contents", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + extension = await response.parse() + assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_upload(self, async_client: AsyncKernel) -> None: + async with async_client.extensions.with_streaming_response.upload( + file=b"raw file contents", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + extension = await response.parse() + assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) + + assert cast(Any, response.is_closed) is True From 0f50b971d4a7b6b6e38a7e43015c3ff8da884e0e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 18:50:59 +0000 Subject: [PATCH 194/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 3d26904f..8f3e0a49 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.14.2" + ".": "0.15.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index c2e14c5d..0f8cb140 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.14.2" +version = "0.15.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index dfcce591..e65ca7f2 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.14.2" # x-release-please-version +__version__ = "0.15.0" # x-release-please-version From d2106d46694c1933ee2eacef5644d0c33b4589b7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 18 Oct 2025 02:09:41 +0000 Subject: [PATCH 195/448] chore: bump `httpx-aiohttp` version to 0.1.9 --- pyproject.toml | 2 +- requirements-dev.lock | 2 +- requirements.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0f8cb140..0c6c3eb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ Homepage = "https://github.com/onkernel/kernel-python-sdk" Repository = "https://github.com/onkernel/kernel-python-sdk" [project.optional-dependencies] -aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.8"] +aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.9"] [tool.rye] managed = true diff --git a/requirements-dev.lock b/requirements-dev.lock index 249acb1e..fc032738 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -56,7 +56,7 @@ httpx==0.28.1 # via httpx-aiohttp # via kernel # via respx -httpx-aiohttp==0.1.8 +httpx-aiohttp==0.1.9 # via kernel idna==3.4 # via anyio diff --git a/requirements.lock b/requirements.lock index 49cc9054..ed64e3db 100644 --- a/requirements.lock +++ b/requirements.lock @@ -43,7 +43,7 @@ httpcore==1.0.9 httpx==0.28.1 # via httpx-aiohttp # via kernel -httpx-aiohttp==0.1.8 +httpx-aiohttp==0.1.9 # via kernel idna==3.4 # via anyio From a9e223d097ff4f7611e3abbe7caddb7adf7325ff Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 18:39:34 +0000 Subject: [PATCH 196/448] feat: ad hoc playwright code exec AP| --- .stats.yml | 8 +- api.md | 12 + src/kernel/resources/browsers/__init__.py | 14 ++ src/kernel/resources/browsers/browsers.py | 32 +++ src/kernel/resources/browsers/playwright.py | 205 ++++++++++++++++++ src/kernel/types/browsers/__init__.py | 2 + .../browsers/playwright_execute_params.py | 21 ++ .../browsers/playwright_execute_response.py | 24 ++ .../api_resources/browsers/test_playwright.py | 136 ++++++++++++ 9 files changed, 450 insertions(+), 4 deletions(-) create mode 100644 src/kernel/resources/browsers/playwright.py create mode 100644 src/kernel/types/browsers/playwright_execute_params.py create mode 100644 src/kernel/types/browsers/playwright_execute_response.py create mode 100644 tests/api_resources/browsers/test_playwright.py diff --git a/.stats.yml b/.stats.yml index b4dc6064..8cd6a7c5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 64 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-e21f0324774a1762bc2bba0da3a8a6b0d0e74720d7a1c83dec813f9e027fcf58.yml -openapi_spec_hash: f1b636abfd6cb8e7c2ba7ffb8e53b9ba -config_hash: 09a2df23048cb16689c9a390d9e5bc47 +configured_endpoints: 65 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-ecf484375ede1edd7754779ad8beeebd4ba9118152fe6cd65772dc7245a19dee.yml +openapi_spec_hash: b1f3f05005f75cbf5b82299459e2aa9b +config_hash: 3ded7a0ed77b1bfd68eabc6763521fe8 diff --git a/api.md b/api.md index 858dbfd8..164a8c47 100644 --- a/api.md +++ b/api.md @@ -178,6 +178,18 @@ Methods: - client.browsers.computer.scroll(id, \*\*params) -> None - client.browsers.computer.type_text(id, \*\*params) -> None +## Playwright + +Types: + +```python +from kernel.types.browsers import PlaywrightExecuteResponse +``` + +Methods: + +- client.browsers.playwright.execute(id, \*\*params) -> PlaywrightExecuteResponse + # Profiles Types: diff --git a/src/kernel/resources/browsers/__init__.py b/src/kernel/resources/browsers/__init__.py index abcc8f78..a1acee20 100644 --- a/src/kernel/resources/browsers/__init__.py +++ b/src/kernel/resources/browsers/__init__.py @@ -48,6 +48,14 @@ ComputerResourceWithStreamingResponse, AsyncComputerResourceWithStreamingResponse, ) +from .playwright import ( + PlaywrightResource, + AsyncPlaywrightResource, + PlaywrightResourceWithRawResponse, + AsyncPlaywrightResourceWithRawResponse, + PlaywrightResourceWithStreamingResponse, + AsyncPlaywrightResourceWithStreamingResponse, +) __all__ = [ "ReplaysResource", @@ -80,6 +88,12 @@ "AsyncComputerResourceWithRawResponse", "ComputerResourceWithStreamingResponse", "AsyncComputerResourceWithStreamingResponse", + "PlaywrightResource", + "AsyncPlaywrightResource", + "PlaywrightResourceWithRawResponse", + "AsyncPlaywrightResourceWithRawResponse", + "PlaywrightResourceWithStreamingResponse", + "AsyncPlaywrightResourceWithStreamingResponse", "BrowsersResource", "AsyncBrowsersResource", "BrowsersResourceWithRawResponse", diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index c65a738a..6a0129c0 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -50,6 +50,14 @@ AsyncComputerResourceWithStreamingResponse, ) from ..._compat import cached_property +from .playwright import ( + PlaywrightResource, + AsyncPlaywrightResource, + PlaywrightResourceWithRawResponse, + AsyncPlaywrightResourceWithRawResponse, + PlaywrightResourceWithStreamingResponse, + AsyncPlaywrightResourceWithStreamingResponse, +) from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( to_raw_response_wrapper, @@ -87,6 +95,10 @@ def logs(self) -> LogsResource: def computer(self) -> ComputerResource: return ComputerResource(self._client) + @cached_property + def playwright(self) -> PlaywrightResource: + return PlaywrightResource(self._client) + @cached_property def with_raw_response(self) -> BrowsersResourceWithRawResponse: """ @@ -391,6 +403,10 @@ def logs(self) -> AsyncLogsResource: def computer(self) -> AsyncComputerResource: return AsyncComputerResource(self._client) + @cached_property + def playwright(self) -> AsyncPlaywrightResource: + return AsyncPlaywrightResource(self._client) + @cached_property def with_raw_response(self) -> AsyncBrowsersResourceWithRawResponse: """ @@ -719,6 +735,10 @@ def logs(self) -> LogsResourceWithRawResponse: def computer(self) -> ComputerResourceWithRawResponse: return ComputerResourceWithRawResponse(self._browsers.computer) + @cached_property + def playwright(self) -> PlaywrightResourceWithRawResponse: + return PlaywrightResourceWithRawResponse(self._browsers.playwright) + class AsyncBrowsersResourceWithRawResponse: def __init__(self, browsers: AsyncBrowsersResource) -> None: @@ -763,6 +783,10 @@ def logs(self) -> AsyncLogsResourceWithRawResponse: def computer(self) -> AsyncComputerResourceWithRawResponse: return AsyncComputerResourceWithRawResponse(self._browsers.computer) + @cached_property + def playwright(self) -> AsyncPlaywrightResourceWithRawResponse: + return AsyncPlaywrightResourceWithRawResponse(self._browsers.playwright) + class BrowsersResourceWithStreamingResponse: def __init__(self, browsers: BrowsersResource) -> None: @@ -807,6 +831,10 @@ def logs(self) -> LogsResourceWithStreamingResponse: def computer(self) -> ComputerResourceWithStreamingResponse: return ComputerResourceWithStreamingResponse(self._browsers.computer) + @cached_property + def playwright(self) -> PlaywrightResourceWithStreamingResponse: + return PlaywrightResourceWithStreamingResponse(self._browsers.playwright) + class AsyncBrowsersResourceWithStreamingResponse: def __init__(self, browsers: AsyncBrowsersResource) -> None: @@ -850,3 +878,7 @@ def logs(self) -> AsyncLogsResourceWithStreamingResponse: @cached_property def computer(self) -> AsyncComputerResourceWithStreamingResponse: return AsyncComputerResourceWithStreamingResponse(self._browsers.computer) + + @cached_property + def playwright(self) -> AsyncPlaywrightResourceWithStreamingResponse: + return AsyncPlaywrightResourceWithStreamingResponse(self._browsers.playwright) diff --git a/src/kernel/resources/browsers/playwright.py b/src/kernel/resources/browsers/playwright.py new file mode 100644 index 00000000..c168a4a7 --- /dev/null +++ b/src/kernel/resources/browsers/playwright.py @@ -0,0 +1,205 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from ..._utils import maybe_transform, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..._base_client import make_request_options +from ...types.browsers import playwright_execute_params +from ...types.browsers.playwright_execute_response import PlaywrightExecuteResponse + +__all__ = ["PlaywrightResource", "AsyncPlaywrightResource"] + + +class PlaywrightResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> PlaywrightResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return PlaywrightResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> PlaywrightResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return PlaywrightResourceWithStreamingResponse(self) + + def execute( + self, + id: str, + *, + code: str, + timeout_sec: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> PlaywrightExecuteResponse: + """ + Execute arbitrary Playwright code in a fresh execution context against the + browser. The code runs in the same VM as the browser, minimizing latency and + maximizing throughput. It has access to 'page', 'context', and 'browser' + variables. It can `return` a value, and this value is returned in the response. + + Args: + code: TypeScript/JavaScript code to execute. The code has access to 'page', 'context', + and 'browser' variables. It runs within a function, so you can use a return + statement at the end to return a value. This value is returned as the `result` + property in the response. Example: "await page.goto('https://example.com'); + return await page.title();" + + timeout_sec: Maximum execution time in seconds. Default is 60. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/browsers/{id}/playwright/execute", + body=maybe_transform( + { + "code": code, + "timeout_sec": timeout_sec, + }, + playwright_execute_params.PlaywrightExecuteParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=PlaywrightExecuteResponse, + ) + + +class AsyncPlaywrightResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncPlaywrightResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncPlaywrightResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncPlaywrightResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return AsyncPlaywrightResourceWithStreamingResponse(self) + + async def execute( + self, + id: str, + *, + code: str, + timeout_sec: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> PlaywrightExecuteResponse: + """ + Execute arbitrary Playwright code in a fresh execution context against the + browser. The code runs in the same VM as the browser, minimizing latency and + maximizing throughput. It has access to 'page', 'context', and 'browser' + variables. It can `return` a value, and this value is returned in the response. + + Args: + code: TypeScript/JavaScript code to execute. The code has access to 'page', 'context', + and 'browser' variables. It runs within a function, so you can use a return + statement at the end to return a value. This value is returned as the `result` + property in the response. Example: "await page.goto('https://example.com'); + return await page.title();" + + timeout_sec: Maximum execution time in seconds. Default is 60. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/browsers/{id}/playwright/execute", + body=await async_maybe_transform( + { + "code": code, + "timeout_sec": timeout_sec, + }, + playwright_execute_params.PlaywrightExecuteParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=PlaywrightExecuteResponse, + ) + + +class PlaywrightResourceWithRawResponse: + def __init__(self, playwright: PlaywrightResource) -> None: + self._playwright = playwright + + self.execute = to_raw_response_wrapper( + playwright.execute, + ) + + +class AsyncPlaywrightResourceWithRawResponse: + def __init__(self, playwright: AsyncPlaywrightResource) -> None: + self._playwright = playwright + + self.execute = async_to_raw_response_wrapper( + playwright.execute, + ) + + +class PlaywrightResourceWithStreamingResponse: + def __init__(self, playwright: PlaywrightResource) -> None: + self._playwright = playwright + + self.execute = to_streamed_response_wrapper( + playwright.execute, + ) + + +class AsyncPlaywrightResourceWithStreamingResponse: + def __init__(self, playwright: AsyncPlaywrightResource) -> None: + self._playwright = playwright + + self.execute = async_to_streamed_response_wrapper( + playwright.execute, + ) diff --git a/src/kernel/types/browsers/__init__.py b/src/kernel/types/browsers/__init__.py index 9b0ed53a..f8a263c1 100644 --- a/src/kernel/types/browsers/__init__.py +++ b/src/kernel/types/browsers/__init__.py @@ -31,9 +31,11 @@ from .f_create_directory_params import FCreateDirectoryParams as FCreateDirectoryParams from .f_delete_directory_params import FDeleteDirectoryParams as FDeleteDirectoryParams from .f_download_dir_zip_params import FDownloadDirZipParams as FDownloadDirZipParams +from .playwright_execute_params import PlaywrightExecuteParams as PlaywrightExecuteParams from .computer_drag_mouse_params import ComputerDragMouseParams as ComputerDragMouseParams from .computer_move_mouse_params import ComputerMoveMouseParams as ComputerMoveMouseParams from .computer_click_mouse_params import ComputerClickMouseParams as ComputerClickMouseParams +from .playwright_execute_response import PlaywrightExecuteResponse as PlaywrightExecuteResponse from .f_set_file_permissions_params import FSetFilePermissionsParams as FSetFilePermissionsParams from .process_stdout_stream_response import ProcessStdoutStreamResponse as ProcessStdoutStreamResponse from .computer_capture_screenshot_params import ComputerCaptureScreenshotParams as ComputerCaptureScreenshotParams diff --git a/src/kernel/types/browsers/playwright_execute_params.py b/src/kernel/types/browsers/playwright_execute_params.py new file mode 100644 index 00000000..948a74c1 --- /dev/null +++ b/src/kernel/types/browsers/playwright_execute_params.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["PlaywrightExecuteParams"] + + +class PlaywrightExecuteParams(TypedDict, total=False): + code: Required[str] + """TypeScript/JavaScript code to execute. + + The code has access to 'page', 'context', and 'browser' variables. It runs + within a function, so you can use a return statement at the end to return a + value. This value is returned as the `result` property in the response. Example: + "await page.goto('https://example.com'); return await page.title();" + """ + + timeout_sec: int + """Maximum execution time in seconds. Default is 60.""" diff --git a/src/kernel/types/browsers/playwright_execute_response.py b/src/kernel/types/browsers/playwright_execute_response.py new file mode 100644 index 00000000..a805ba85 --- /dev/null +++ b/src/kernel/types/browsers/playwright_execute_response.py @@ -0,0 +1,24 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = ["PlaywrightExecuteResponse"] + + +class PlaywrightExecuteResponse(BaseModel): + success: bool + """Whether the code executed successfully""" + + error: Optional[str] = None + """Error message if execution failed""" + + result: Optional[object] = None + """The value returned by the code (if any)""" + + stderr: Optional[str] = None + """Standard error from the execution""" + + stdout: Optional[str] = None + """Standard output from the execution""" diff --git a/tests/api_resources/browsers/test_playwright.py b/tests/api_resources/browsers/test_playwright.py new file mode 100644 index 00000000..cb79410c --- /dev/null +++ b/tests/api_resources/browsers/test_playwright.py @@ -0,0 +1,136 @@ +# 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 kernel import Kernel, AsyncKernel +from tests.utils import assert_matches_type +from kernel.types.browsers import PlaywrightExecuteResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestPlaywright: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_execute(self, client: Kernel) -> None: + playwright = client.browsers.playwright.execute( + id="id", + code="code", + ) + assert_matches_type(PlaywrightExecuteResponse, playwright, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_execute_with_all_params(self, client: Kernel) -> None: + playwright = client.browsers.playwright.execute( + id="id", + code="code", + timeout_sec=1, + ) + assert_matches_type(PlaywrightExecuteResponse, playwright, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_execute(self, client: Kernel) -> None: + response = client.browsers.playwright.with_raw_response.execute( + id="id", + code="code", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + playwright = response.parse() + assert_matches_type(PlaywrightExecuteResponse, playwright, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_execute(self, client: Kernel) -> None: + with client.browsers.playwright.with_streaming_response.execute( + id="id", + code="code", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + playwright = response.parse() + assert_matches_type(PlaywrightExecuteResponse, playwright, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_execute(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.playwright.with_raw_response.execute( + id="", + code="code", + ) + + +class TestAsyncPlaywright: + 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_execute(self, async_client: AsyncKernel) -> None: + playwright = await async_client.browsers.playwright.execute( + id="id", + code="code", + ) + assert_matches_type(PlaywrightExecuteResponse, playwright, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_execute_with_all_params(self, async_client: AsyncKernel) -> None: + playwright = await async_client.browsers.playwright.execute( + id="id", + code="code", + timeout_sec=1, + ) + assert_matches_type(PlaywrightExecuteResponse, playwright, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_execute(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.playwright.with_raw_response.execute( + id="id", + code="code", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + playwright = await response.parse() + assert_matches_type(PlaywrightExecuteResponse, playwright, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_execute(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.playwright.with_streaming_response.execute( + id="id", + code="code", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + playwright = await response.parse() + assert_matches_type(PlaywrightExecuteResponse, playwright, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_execute(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.playwright.with_raw_response.execute( + id="", + code="code", + ) From cfe9152480789de797ad03a43bd32b128ca0761b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 18:41:50 +0000 Subject: [PATCH 197/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 8f3e0a49..b4e9013b 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.15.0" + ".": "0.16.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 0c6c3eb0..18317f85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.15.0" +version = "0.16.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index e65ca7f2..211e253d 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.15.0" # x-release-please-version +__version__ = "0.16.0" # x-release-please-version From a9ca87bdb580caa74d866bdc4e017721a0eab616 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 19:40:46 +0000 Subject: [PATCH 198/448] feat: Use ping instead of bright data for ISP proxy --- .stats.yml | 4 ++-- src/kernel/types/proxy_create_params.py | 8 ++++---- src/kernel/types/proxy_create_response.py | 8 ++++---- src/kernel/types/proxy_list_response.py | 8 ++++---- src/kernel/types/proxy_retrieve_response.py | 8 ++++---- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.stats.yml b/.stats.yml index 8cd6a7c5..8cf9ee75 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 65 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-ecf484375ede1edd7754779ad8beeebd4ba9118152fe6cd65772dc7245a19dee.yml -openapi_spec_hash: b1f3f05005f75cbf5b82299459e2aa9b +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-e914e2d08b888c77051acb09176d5e88052f130e0d22e85d946a675d2c3d39ab.yml +openapi_spec_hash: 611d0ed1b4519331470b5d14e5f6784a config_hash: 3ded7a0ed77b1bfd68eabc6763521fe8 diff --git a/src/kernel/types/proxy_create_params.py b/src/kernel/types/proxy_create_params.py index 1f8d4b7d..485df606 100644 --- a/src/kernel/types/proxy_create_params.py +++ b/src/kernel/types/proxy_create_params.py @@ -35,13 +35,13 @@ class ProxyCreateParams(TypedDict, total=False): class ConfigDatacenterProxyConfig(TypedDict, total=False): - country: Required[str] - """ISO 3166 country code.""" + country: str + """ISO 3166 country code. Defaults to US if not provided.""" class ConfigIspProxyConfig(TypedDict, total=False): - country: Required[str] - """ISO 3166 country code.""" + country: str + """ISO 3166 country code. Defaults to US if not provided.""" class ConfigResidentialProxyConfig(TypedDict, total=False): diff --git a/src/kernel/types/proxy_create_response.py b/src/kernel/types/proxy_create_response.py index 831c45f3..6ec2f7f9 100644 --- a/src/kernel/types/proxy_create_response.py +++ b/src/kernel/types/proxy_create_response.py @@ -18,13 +18,13 @@ class ConfigDatacenterProxyConfig(BaseModel): - country: str - """ISO 3166 country code.""" + country: Optional[str] = None + """ISO 3166 country code. Defaults to US if not provided.""" class ConfigIspProxyConfig(BaseModel): - country: str - """ISO 3166 country code.""" + country: Optional[str] = None + """ISO 3166 country code. Defaults to US if not provided.""" class ConfigResidentialProxyConfig(BaseModel): diff --git a/src/kernel/types/proxy_list_response.py b/src/kernel/types/proxy_list_response.py index 96488816..e4abb0d8 100644 --- a/src/kernel/types/proxy_list_response.py +++ b/src/kernel/types/proxy_list_response.py @@ -19,13 +19,13 @@ class ProxyListResponseItemConfigDatacenterProxyConfig(BaseModel): - country: str - """ISO 3166 country code.""" + country: Optional[str] = None + """ISO 3166 country code. Defaults to US if not provided.""" class ProxyListResponseItemConfigIspProxyConfig(BaseModel): - country: str - """ISO 3166 country code.""" + country: Optional[str] = None + """ISO 3166 country code. Defaults to US if not provided.""" class ProxyListResponseItemConfigResidentialProxyConfig(BaseModel): diff --git a/src/kernel/types/proxy_retrieve_response.py b/src/kernel/types/proxy_retrieve_response.py index 4c2d63cc..5262fc48 100644 --- a/src/kernel/types/proxy_retrieve_response.py +++ b/src/kernel/types/proxy_retrieve_response.py @@ -18,13 +18,13 @@ class ConfigDatacenterProxyConfig(BaseModel): - country: str - """ISO 3166 country code.""" + country: Optional[str] = None + """ISO 3166 country code. Defaults to US if not provided.""" class ConfigIspProxyConfig(BaseModel): - country: str - """ISO 3166 country code.""" + country: Optional[str] = None + """ISO 3166 country code. Defaults to US if not provided.""" class ConfigResidentialProxyConfig(BaseModel): From 29a8754c0d9aefc49350825092541abf0b5f5a64 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 19:48:51 +0000 Subject: [PATCH 199/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index b4e9013b..6db19b95 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.16.0" + ".": "0.17.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 18317f85..1a56293c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.16.0" +version = "0.17.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 211e253d..123dd308 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.16.0" # x-release-please-version +__version__ = "0.17.0" # x-release-please-version From ac7312405ca272bc08427b1c5bfba8bf059d4b96 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 02:16:35 +0000 Subject: [PATCH 200/448] fix(client): close streams without requiring full consumption --- src/kernel/_streaming.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/kernel/_streaming.py b/src/kernel/_streaming.py index e3131a3b..e6d03062 100644 --- a/src/kernel/_streaming.py +++ b/src/kernel/_streaming.py @@ -57,9 +57,8 @@ def __stream__(self) -> Iterator[_T]: for sse in iterator: yield process_data(data=sse.json(), cast_to=cast_to, response=response) - # Ensure the entire stream is consumed - for _sse in iterator: - ... + # As we might not fully consume the response stream, we need to close it explicitly + response.close() def __enter__(self) -> Self: return self @@ -121,9 +120,8 @@ async def __stream__(self) -> AsyncIterator[_T]: async for sse in iterator: yield process_data(data=sse.json(), cast_to=cast_to, response=response) - # Ensure the entire stream is consumed - async for _sse in iterator: - ... + # As we might not fully consume the response stream, we need to close it explicitly + await response.aclose() async def __aenter__(self) -> Self: return self From 5471ae42c21871d2d94ee166d701527af6e2b8e2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 21:01:37 +0000 Subject: [PATCH 201/448] feat: apps: add offset pagination + headers --- .stats.yml | 6 ++--- api.md | 2 +- src/kernel/resources/apps.py | 39 ++++++++++++++++++++------- src/kernel/types/app_list_params.py | 6 +++++ src/kernel/types/app_list_response.py | 9 +++---- tests/api_resources/test_apps.py | 21 +++++++++------ 6 files changed, 55 insertions(+), 28 deletions(-) diff --git a/.stats.yml b/.stats.yml index 8cf9ee75..9e4dbe62 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 65 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-e914e2d08b888c77051acb09176d5e88052f130e0d22e85d946a675d2c3d39ab.yml -openapi_spec_hash: 611d0ed1b4519331470b5d14e5f6784a -config_hash: 3ded7a0ed77b1bfd68eabc6763521fe8 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-015c11efc34c81d4d82a937c017f5eb789ea3ca21a05b70e2ed31c069b839293.yml +openapi_spec_hash: 3dcab2044da305f376cecf4eca38caee +config_hash: 0fbdda3a736cc2748ca33371871e61b3 diff --git a/api.md b/api.md index 164a8c47..aa2ef0e8 100644 --- a/api.md +++ b/api.md @@ -35,7 +35,7 @@ from kernel.types import AppListResponse Methods: -- client.apps.list(\*\*params) -> AppListResponse +- client.apps.list(\*\*params) -> SyncOffsetPagination[AppListResponse] # Invocations diff --git a/src/kernel/resources/apps.py b/src/kernel/resources/apps.py index 28117b98..34aa1299 100644 --- a/src/kernel/resources/apps.py +++ b/src/kernel/resources/apps.py @@ -6,7 +6,7 @@ from ..types import app_list_params from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._utils import maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -15,7 +15,8 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from .._base_client import make_request_options +from ..pagination import SyncOffsetPagination, AsyncOffsetPagination +from .._base_client import AsyncPaginator, make_request_options from ..types.app_list_response import AppListResponse __all__ = ["AppsResource", "AsyncAppsResource"] @@ -45,6 +46,8 @@ def list( self, *, app_name: str | Omit = omit, + limit: int | Omit = omit, + offset: int | Omit = omit, version: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -52,7 +55,7 @@ def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> AppListResponse: + ) -> SyncOffsetPagination[AppListResponse]: """List applications. Optionally filter by app name and/or version label. @@ -60,6 +63,10 @@ def list( Args: app_name: Filter results by application name. + limit: Limit the number of app to return. + + offset: Offset the number of app to return. + version: Filter results by version label. extra_headers: Send extra headers @@ -70,8 +77,9 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ - return self._get( + return self._get_api_list( "/apps", + page=SyncOffsetPagination[AppListResponse], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -80,12 +88,14 @@ def list( query=maybe_transform( { "app_name": app_name, + "limit": limit, + "offset": offset, "version": version, }, app_list_params.AppListParams, ), ), - cast_to=AppListResponse, + model=AppListResponse, ) @@ -109,10 +119,12 @@ def with_streaming_response(self) -> AsyncAppsResourceWithStreamingResponse: """ return AsyncAppsResourceWithStreamingResponse(self) - async def list( + def list( self, *, app_name: str | Omit = omit, + limit: int | Omit = omit, + offset: int | Omit = omit, version: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -120,7 +132,7 @@ async def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> AppListResponse: + ) -> AsyncPaginator[AppListResponse, AsyncOffsetPagination[AppListResponse]]: """List applications. Optionally filter by app name and/or version label. @@ -128,6 +140,10 @@ async def list( Args: app_name: Filter results by application name. + limit: Limit the number of app to return. + + offset: Offset the number of app to return. + version: Filter results by version label. extra_headers: Send extra headers @@ -138,22 +154,25 @@ async def list( timeout: Override the client-level default timeout for this request, in seconds """ - return await self._get( + return self._get_api_list( "/apps", + page=AsyncOffsetPagination[AppListResponse], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout, - query=await async_maybe_transform( + query=maybe_transform( { "app_name": app_name, + "limit": limit, + "offset": offset, "version": version, }, app_list_params.AppListParams, ), ), - cast_to=AppListResponse, + model=AppListResponse, ) diff --git a/src/kernel/types/app_list_params.py b/src/kernel/types/app_list_params.py index d4506a3e..1e2f5278 100644 --- a/src/kernel/types/app_list_params.py +++ b/src/kernel/types/app_list_params.py @@ -11,5 +11,11 @@ class AppListParams(TypedDict, total=False): app_name: str """Filter results by application name.""" + limit: int + """Limit the number of app to return.""" + + offset: int + """Offset the number of app to return.""" + version: str """Filter results by version label.""" diff --git a/src/kernel/types/app_list_response.py b/src/kernel/types/app_list_response.py index bdbc3e61..56a2d4bd 100644 --- a/src/kernel/types/app_list_response.py +++ b/src/kernel/types/app_list_response.py @@ -1,15 +1,15 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import Dict, List -from typing_extensions import Literal, TypeAlias +from typing_extensions import Literal from .._models import BaseModel from .shared.app_action import AppAction -__all__ = ["AppListResponse", "AppListResponseItem"] +__all__ = ["AppListResponse"] -class AppListResponseItem(BaseModel): +class AppListResponse(BaseModel): id: str """Unique identifier for the app version""" @@ -30,6 +30,3 @@ class AppListResponseItem(BaseModel): version: str """Version label for the application""" - - -AppListResponse: TypeAlias = List[AppListResponseItem] diff --git a/tests/api_resources/test_apps.py b/tests/api_resources/test_apps.py index 5e6db3ba..7475bcd5 100644 --- a/tests/api_resources/test_apps.py +++ b/tests/api_resources/test_apps.py @@ -10,6 +10,7 @@ from kernel import Kernel, AsyncKernel from tests.utils import assert_matches_type from kernel.types import AppListResponse +from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -21,16 +22,18 @@ class TestApps: @parametrize def test_method_list(self, client: Kernel) -> None: app = client.apps.list() - assert_matches_type(AppListResponse, app, path=["response"]) + assert_matches_type(SyncOffsetPagination[AppListResponse], app, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_list_with_all_params(self, client: Kernel) -> None: app = client.apps.list( app_name="app_name", + limit=1, + offset=0, version="version", ) - assert_matches_type(AppListResponse, app, path=["response"]) + assert_matches_type(SyncOffsetPagination[AppListResponse], app, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize @@ -40,7 +43,7 @@ def test_raw_response_list(self, client: Kernel) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" app = response.parse() - assert_matches_type(AppListResponse, app, path=["response"]) + assert_matches_type(SyncOffsetPagination[AppListResponse], app, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize @@ -50,7 +53,7 @@ def test_streaming_response_list(self, client: Kernel) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" app = response.parse() - assert_matches_type(AppListResponse, app, path=["response"]) + assert_matches_type(SyncOffsetPagination[AppListResponse], app, path=["response"]) assert cast(Any, response.is_closed) is True @@ -64,16 +67,18 @@ class TestAsyncApps: @parametrize async def test_method_list(self, async_client: AsyncKernel) -> None: app = await async_client.apps.list() - assert_matches_type(AppListResponse, app, path=["response"]) + assert_matches_type(AsyncOffsetPagination[AppListResponse], app, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: app = await async_client.apps.list( app_name="app_name", + limit=1, + offset=0, version="version", ) - assert_matches_type(AppListResponse, app, path=["response"]) + assert_matches_type(AsyncOffsetPagination[AppListResponse], app, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize @@ -83,7 +88,7 @@ async def test_raw_response_list(self, async_client: AsyncKernel) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" app = await response.parse() - assert_matches_type(AppListResponse, app, path=["response"]) + assert_matches_type(AsyncOffsetPagination[AppListResponse], app, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize @@ -93,6 +98,6 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" app = await response.parse() - assert_matches_type(AppListResponse, app, path=["response"]) + assert_matches_type(AsyncOffsetPagination[AppListResponse], app, path=["response"]) assert cast(Any, response.is_closed) is True From f00ab8f4ae72393262a17d5d25fd82875ca537b5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 21:20:08 +0000 Subject: [PATCH 202/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 6db19b95..4ad3fef3 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.17.0" + ".": "0.18.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 1a56293c..737af0ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.17.0" +version = "0.18.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 123dd308..abb53bdb 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.17.0" # x-release-please-version +__version__ = "0.18.0" # x-release-please-version From 35197201bdcacdf5d86f1d16536e2c167116f7be Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 02:40:59 +0000 Subject: [PATCH 203/448] chore(internal/tests): avoid race condition with implicit client cleanup --- tests/test_client.py | 366 ++++++++++++++++++++++++------------------- 1 file changed, 202 insertions(+), 164 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 15329aee..95661e8a 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -59,51 +59,49 @@ def _get_open_connections(client: Kernel | AsyncKernel) -> int: class TestKernel: - client = Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) - @pytest.mark.respx(base_url=base_url) - def test_raw_response(self, respx_mock: MockRouter) -> None: + def test_raw_response(self, respx_mock: MockRouter, client: Kernel) -> None: respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = self.client.post("/foo", cast_to=httpx.Response) + response = client.post("/foo", cast_to=httpx.Response) assert response.status_code == 200 assert isinstance(response, httpx.Response) assert response.json() == {"foo": "bar"} @pytest.mark.respx(base_url=base_url) - def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None: + def test_raw_response_for_binary(self, respx_mock: MockRouter, client: Kernel) -> None: respx_mock.post("/foo").mock( return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}') ) - response = self.client.post("/foo", cast_to=httpx.Response) + response = client.post("/foo", cast_to=httpx.Response) assert response.status_code == 200 assert isinstance(response, httpx.Response) assert response.json() == {"foo": "bar"} - def test_copy(self) -> None: - copied = self.client.copy() - assert id(copied) != id(self.client) + def test_copy(self, client: Kernel) -> None: + copied = client.copy() + assert id(copied) != id(client) - copied = self.client.copy(api_key="another My API Key") + copied = client.copy(api_key="another My API Key") assert copied.api_key == "another My API Key" - assert self.client.api_key == "My API Key" + assert client.api_key == "My API Key" - def test_copy_default_options(self) -> None: + def test_copy_default_options(self, client: Kernel) -> None: # options that have a default are overridden correctly - copied = self.client.copy(max_retries=7) + copied = client.copy(max_retries=7) assert copied.max_retries == 7 - assert self.client.max_retries == 2 + assert client.max_retries == 2 copied2 = copied.copy(max_retries=6) assert copied2.max_retries == 6 assert copied.max_retries == 7 # timeout - assert isinstance(self.client.timeout, httpx.Timeout) - copied = self.client.copy(timeout=None) + assert isinstance(client.timeout, httpx.Timeout) + copied = client.copy(timeout=None) assert copied.timeout is None - assert isinstance(self.client.timeout, httpx.Timeout) + assert isinstance(client.timeout, httpx.Timeout) def test_copy_default_headers(self) -> None: client = Kernel( @@ -138,6 +136,7 @@ def test_copy_default_headers(self) -> None: match="`default_headers` and `set_default_headers` arguments are mutually exclusive", ): client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"}) + client.close() def test_copy_default_query(self) -> None: client = Kernel( @@ -175,13 +174,15 @@ def test_copy_default_query(self) -> None: ): client.copy(set_default_query={}, default_query={"foo": "Bar"}) - def test_copy_signature(self) -> None: + client.close() + + def test_copy_signature(self, client: Kernel) -> None: # ensure the same parameters that can be passed to the client are defined in the `.copy()` method init_signature = inspect.signature( # mypy doesn't like that we access the `__init__` property. - self.client.__init__, # type: ignore[misc] + client.__init__, # type: ignore[misc] ) - copy_signature = inspect.signature(self.client.copy) + copy_signature = inspect.signature(client.copy) exclude_params = {"transport", "proxies", "_strict_response_validation"} for name in init_signature.parameters.keys(): @@ -192,12 +193,12 @@ def test_copy_signature(self) -> None: assert copy_param is not None, f"copy() signature is missing the {name} param" @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") - def test_copy_build_request(self) -> None: + def test_copy_build_request(self, client: Kernel) -> None: options = FinalRequestOptions(method="get", url="/foo") def build_request(options: FinalRequestOptions) -> None: - client = self.client.copy() - client._build_request(options) + client_copy = client.copy() + client_copy._build_request(options) # ensure that the machinery is warmed up before tracing starts. build_request(options) @@ -254,14 +255,12 @@ def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.Statistic print(frame) raise AssertionError() - def test_request_timeout(self) -> None: - request = self.client._build_request(FinalRequestOptions(method="get", url="/foo")) + def test_request_timeout(self, client: Kernel) -> None: + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT - request = self.client._build_request( - FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0)) - ) + request = client._build_request(FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0))) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == httpx.Timeout(100.0) @@ -272,6 +271,8 @@ def test_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == httpx.Timeout(0) + client.close() + def test_http_client_timeout_option(self) -> None: # custom timeout given to the httpx client should be used with httpx.Client(timeout=None) as http_client: @@ -283,6 +284,8 @@ def test_http_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == httpx.Timeout(None) + client.close() + # no timeout given to the httpx client should not use the httpx default with httpx.Client() as http_client: client = Kernel( @@ -293,6 +296,8 @@ def test_http_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT + client.close() + # explicitly passing the default timeout currently results in it being ignored with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: client = Kernel( @@ -303,6 +308,8 @@ def test_http_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT # our default + client.close() + async def test_invalid_http_client(self) -> None: with pytest.raises(TypeError, match="Invalid `http_client` arg"): async with httpx.AsyncClient() as http_client: @@ -314,14 +321,14 @@ async def test_invalid_http_client(self) -> None: ) def test_default_headers_option(self) -> None: - client = Kernel( + test_client = Kernel( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + request = test_client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "bar" assert request.headers.get("x-stainless-lang") == "python" - client2 = Kernel( + test_client2 = Kernel( base_url=base_url, api_key=api_key, _strict_response_validation=True, @@ -330,10 +337,13 @@ def test_default_headers_option(self) -> None: "X-Stainless-Lang": "my-overriding-header", }, ) - request = client2._build_request(FinalRequestOptions(method="get", url="/foo")) + request = test_client2._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "stainless" assert request.headers.get("x-stainless-lang") == "my-overriding-header" + test_client.close() + test_client2.close() + def test_validate_headers(self) -> None: client = Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -362,8 +372,10 @@ def test_default_query_option(self) -> None: url = httpx.URL(request.url) assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} - def test_request_extra_json(self) -> None: - request = self.client._build_request( + client.close() + + def test_request_extra_json(self, client: Kernel) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -374,7 +386,7 @@ def test_request_extra_json(self) -> None: data = json.loads(request.content.decode("utf-8")) assert data == {"foo": "bar", "baz": False} - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -385,7 +397,7 @@ def test_request_extra_json(self) -> None: assert data == {"baz": False} # `extra_json` takes priority over `json_data` when keys clash - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -396,8 +408,8 @@ def test_request_extra_json(self) -> None: data = json.loads(request.content.decode("utf-8")) assert data == {"foo": "bar", "baz": None} - def test_request_extra_headers(self) -> None: - request = self.client._build_request( + def test_request_extra_headers(self, client: Kernel) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -407,7 +419,7 @@ def test_request_extra_headers(self) -> None: assert request.headers.get("X-Foo") == "Foo" # `extra_headers` takes priority over `default_headers` when keys clash - request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request( + request = client.with_options(default_headers={"X-Bar": "true"})._build_request( FinalRequestOptions( method="post", url="/foo", @@ -418,8 +430,8 @@ def test_request_extra_headers(self) -> None: ) assert request.headers.get("X-Bar") == "false" - def test_request_extra_query(self) -> None: - request = self.client._build_request( + def test_request_extra_query(self, client: Kernel) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -432,7 +444,7 @@ def test_request_extra_query(self) -> None: assert params == {"my_query_param": "Foo"} # if both `query` and `extra_query` are given, they are merged - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -446,7 +458,7 @@ def test_request_extra_query(self) -> None: assert params == {"bar": "1", "foo": "2"} # `extra_query` takes priority over `query` when keys clash - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -489,7 +501,7 @@ def test_multipart_repeating_array(self, client: Kernel) -> None: ] @pytest.mark.respx(base_url=base_url) - def test_basic_union_response(self, respx_mock: MockRouter) -> None: + def test_basic_union_response(self, respx_mock: MockRouter, client: Kernel) -> None: class Model1(BaseModel): name: str @@ -498,12 +510,12 @@ class Model2(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) assert isinstance(response, Model2) assert response.foo == "bar" @pytest.mark.respx(base_url=base_url) - def test_union_response_different_types(self, respx_mock: MockRouter) -> None: + def test_union_response_different_types(self, respx_mock: MockRouter, client: Kernel) -> None: """Union of objects with the same field name using a different type""" class Model1(BaseModel): @@ -514,18 +526,18 @@ class Model2(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) assert isinstance(response, Model2) assert response.foo == "bar" respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1})) - response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) assert isinstance(response, Model1) assert response.foo == 1 @pytest.mark.respx(base_url=base_url) - def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None: + def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter, client: Kernel) -> None: """ Response that sets Content-Type to something other than application/json but returns json data """ @@ -541,7 +553,7 @@ class Model(BaseModel): ) ) - response = self.client.get("/foo", cast_to=Model) + response = client.get("/foo", cast_to=Model) assert isinstance(response, Model) assert response.foo == 2 @@ -553,6 +565,8 @@ def test_base_url_setter(self) -> None: assert client.base_url == "https://example.com/from_setter/" + client.close() + def test_base_url_env(self) -> None: with update_env(KERNEL_BASE_URL="http://localhost:5000/from/env"): client = Kernel(api_key=api_key, _strict_response_validation=True) @@ -566,6 +580,8 @@ def test_base_url_env(self) -> None: client = Kernel(base_url=None, api_key=api_key, _strict_response_validation=True, environment="production") assert str(client.base_url).startswith("https://api.onkernel.com/") + client.close() + @pytest.mark.parametrize( "client", [ @@ -588,6 +604,7 @@ def test_base_url_trailing_slash(self, client: Kernel) -> None: ), ) assert request.url == "http://localhost:5000/custom/path/foo" + client.close() @pytest.mark.parametrize( "client", @@ -611,6 +628,7 @@ def test_base_url_no_trailing_slash(self, client: Kernel) -> None: ), ) assert request.url == "http://localhost:5000/custom/path/foo" + client.close() @pytest.mark.parametrize( "client", @@ -634,35 +652,36 @@ def test_absolute_request_url(self, client: Kernel) -> None: ), ) assert request.url == "https://myapi.com/foo" + client.close() def test_copied_client_does_not_close_http(self) -> None: - client = Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) - assert not client.is_closed() + test_client = Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) + assert not test_client.is_closed() - copied = client.copy() - assert copied is not client + copied = test_client.copy() + assert copied is not test_client del copied - assert not client.is_closed() + assert not test_client.is_closed() def test_client_context_manager(self) -> None: - client = Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) - with client as c2: - assert c2 is client + test_client = Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) + with test_client as c2: + assert c2 is test_client assert not c2.is_closed() - assert not client.is_closed() - assert client.is_closed() + assert not test_client.is_closed() + assert test_client.is_closed() @pytest.mark.respx(base_url=base_url) - def test_client_response_validation_error(self, respx_mock: MockRouter) -> None: + def test_client_response_validation_error(self, respx_mock: MockRouter, client: Kernel) -> None: class Model(BaseModel): foo: str respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}})) with pytest.raises(APIResponseValidationError) as exc: - self.client.get("/foo", cast_to=Model) + client.get("/foo", cast_to=Model) assert isinstance(exc.value.__cause__, ValidationError) @@ -682,11 +701,14 @@ class Model(BaseModel): with pytest.raises(APIResponseValidationError): strict_client.get("/foo", cast_to=Model) - client = Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=False) + non_strict_client = Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=False) - response = client.get("/foo", cast_to=Model) + response = non_strict_client.get("/foo", cast_to=Model) assert isinstance(response, str) # type: ignore[unreachable] + strict_client.close() + non_strict_client.close() + @pytest.mark.parametrize( "remaining_retries,retry_after,timeout", [ @@ -709,9 +731,9 @@ class Model(BaseModel): ], ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) - def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None: - client = Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) - + def test_parse_retry_after_header( + self, remaining_retries: int, retry_after: str, timeout: float, client: Kernel + ) -> None: headers = httpx.Headers({"retry-after": retry_after}) options = FinalRequestOptions(method="get", url="/foo", max_retries=3) calculated = client._calculate_retry_timeout(remaining_retries, options, headers) @@ -725,7 +747,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, clien with pytest.raises(APITimeoutError): client.browsers.with_streaming_response.create().__enter__() - assert _get_open_connections(self.client) == 0 + assert _get_open_connections(client) == 0 @mock.patch("kernel._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) @@ -734,7 +756,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client with pytest.raises(APIStatusError): client.browsers.with_streaming_response.create().__enter__() - assert _get_open_connections(self.client) == 0 + assert _get_open_connections(client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @mock.patch("kernel._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @@ -836,83 +858,77 @@ def test_default_client_creation(self) -> None: ) @pytest.mark.respx(base_url=base_url) - def test_follow_redirects(self, respx_mock: MockRouter) -> None: + def test_follow_redirects(self, respx_mock: MockRouter, client: Kernel) -> None: # Test that the default follow_redirects=True allows following redirects respx_mock.post("/redirect").mock( return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) ) respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"})) - response = self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) + response = client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) assert response.status_code == 200 assert response.json() == {"status": "ok"} @pytest.mark.respx(base_url=base_url) - def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None: + def test_follow_redirects_disabled(self, respx_mock: MockRouter, client: Kernel) -> None: # Test that follow_redirects=False prevents following redirects respx_mock.post("/redirect").mock( return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) ) with pytest.raises(APIStatusError) as exc_info: - self.client.post( - "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response - ) + client.post("/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response) assert exc_info.value.response.status_code == 302 assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected" class TestAsyncKernel: - client = AsyncKernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) - @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio - async def test_raw_response(self, respx_mock: MockRouter) -> None: + async def test_raw_response(self, respx_mock: MockRouter, async_client: AsyncKernel) -> None: respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = await self.client.post("/foo", cast_to=httpx.Response) + response = await async_client.post("/foo", cast_to=httpx.Response) assert response.status_code == 200 assert isinstance(response, httpx.Response) assert response.json() == {"foo": "bar"} @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio - async def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None: + async def test_raw_response_for_binary(self, respx_mock: MockRouter, async_client: AsyncKernel) -> None: respx_mock.post("/foo").mock( return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}') ) - response = await self.client.post("/foo", cast_to=httpx.Response) + response = await async_client.post("/foo", cast_to=httpx.Response) assert response.status_code == 200 assert isinstance(response, httpx.Response) assert response.json() == {"foo": "bar"} - def test_copy(self) -> None: - copied = self.client.copy() - assert id(copied) != id(self.client) + def test_copy(self, async_client: AsyncKernel) -> None: + copied = async_client.copy() + assert id(copied) != id(async_client) - copied = self.client.copy(api_key="another My API Key") + copied = async_client.copy(api_key="another My API Key") assert copied.api_key == "another My API Key" - assert self.client.api_key == "My API Key" + assert async_client.api_key == "My API Key" - def test_copy_default_options(self) -> None: + def test_copy_default_options(self, async_client: AsyncKernel) -> None: # options that have a default are overridden correctly - copied = self.client.copy(max_retries=7) + copied = async_client.copy(max_retries=7) assert copied.max_retries == 7 - assert self.client.max_retries == 2 + assert async_client.max_retries == 2 copied2 = copied.copy(max_retries=6) assert copied2.max_retries == 6 assert copied.max_retries == 7 # timeout - assert isinstance(self.client.timeout, httpx.Timeout) - copied = self.client.copy(timeout=None) + assert isinstance(async_client.timeout, httpx.Timeout) + copied = async_client.copy(timeout=None) assert copied.timeout is None - assert isinstance(self.client.timeout, httpx.Timeout) + assert isinstance(async_client.timeout, httpx.Timeout) - def test_copy_default_headers(self) -> None: + async def test_copy_default_headers(self) -> None: client = AsyncKernel( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) @@ -945,8 +961,9 @@ def test_copy_default_headers(self) -> None: match="`default_headers` and `set_default_headers` arguments are mutually exclusive", ): client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"}) + await client.close() - def test_copy_default_query(self) -> None: + async def test_copy_default_query(self) -> None: client = AsyncKernel( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} ) @@ -982,13 +999,15 @@ def test_copy_default_query(self) -> None: ): client.copy(set_default_query={}, default_query={"foo": "Bar"}) - def test_copy_signature(self) -> None: + await client.close() + + def test_copy_signature(self, async_client: AsyncKernel) -> None: # ensure the same parameters that can be passed to the client are defined in the `.copy()` method init_signature = inspect.signature( # mypy doesn't like that we access the `__init__` property. - self.client.__init__, # type: ignore[misc] + async_client.__init__, # type: ignore[misc] ) - copy_signature = inspect.signature(self.client.copy) + copy_signature = inspect.signature(async_client.copy) exclude_params = {"transport", "proxies", "_strict_response_validation"} for name in init_signature.parameters.keys(): @@ -999,12 +1018,12 @@ def test_copy_signature(self) -> None: assert copy_param is not None, f"copy() signature is missing the {name} param" @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") - def test_copy_build_request(self) -> None: + def test_copy_build_request(self, async_client: AsyncKernel) -> None: options = FinalRequestOptions(method="get", url="/foo") def build_request(options: FinalRequestOptions) -> None: - client = self.client.copy() - client._build_request(options) + client_copy = async_client.copy() + client_copy._build_request(options) # ensure that the machinery is warmed up before tracing starts. build_request(options) @@ -1061,12 +1080,12 @@ def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.Statistic print(frame) raise AssertionError() - async def test_request_timeout(self) -> None: - request = self.client._build_request(FinalRequestOptions(method="get", url="/foo")) + async def test_request_timeout(self, async_client: AsyncKernel) -> None: + request = async_client._build_request(FinalRequestOptions(method="get", url="/foo")) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT - request = self.client._build_request( + request = async_client._build_request( FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0)) ) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore @@ -1081,6 +1100,8 @@ async def test_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == httpx.Timeout(0) + await client.close() + async def test_http_client_timeout_option(self) -> None: # custom timeout given to the httpx client should be used async with httpx.AsyncClient(timeout=None) as http_client: @@ -1092,6 +1113,8 @@ async def test_http_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == httpx.Timeout(None) + await client.close() + # no timeout given to the httpx client should not use the httpx default async with httpx.AsyncClient() as http_client: client = AsyncKernel( @@ -1102,6 +1125,8 @@ async def test_http_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT + await client.close() + # explicitly passing the default timeout currently results in it being ignored async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: client = AsyncKernel( @@ -1112,6 +1137,8 @@ async def test_http_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT # our default + await client.close() + def test_invalid_http_client(self) -> None: with pytest.raises(TypeError, match="Invalid `http_client` arg"): with httpx.Client() as http_client: @@ -1122,15 +1149,15 @@ def test_invalid_http_client(self) -> None: http_client=cast(Any, http_client), ) - def test_default_headers_option(self) -> None: - client = AsyncKernel( + async def test_default_headers_option(self) -> None: + test_client = AsyncKernel( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + request = test_client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "bar" assert request.headers.get("x-stainless-lang") == "python" - client2 = AsyncKernel( + test_client2 = AsyncKernel( base_url=base_url, api_key=api_key, _strict_response_validation=True, @@ -1139,10 +1166,13 @@ def test_default_headers_option(self) -> None: "X-Stainless-Lang": "my-overriding-header", }, ) - request = client2._build_request(FinalRequestOptions(method="get", url="/foo")) + request = test_client2._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "stainless" assert request.headers.get("x-stainless-lang") == "my-overriding-header" + await test_client.close() + await test_client2.close() + def test_validate_headers(self) -> None: client = AsyncKernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -1153,7 +1183,7 @@ def test_validate_headers(self) -> None: client2 = AsyncKernel(base_url=base_url, api_key=None, _strict_response_validation=True) _ = client2 - def test_default_query_option(self) -> None: + async def test_default_query_option(self) -> None: client = AsyncKernel( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} ) @@ -1171,8 +1201,10 @@ def test_default_query_option(self) -> None: url = httpx.URL(request.url) assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} - def test_request_extra_json(self) -> None: - request = self.client._build_request( + await client.close() + + def test_request_extra_json(self, client: Kernel) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1183,7 +1215,7 @@ def test_request_extra_json(self) -> None: data = json.loads(request.content.decode("utf-8")) assert data == {"foo": "bar", "baz": False} - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1194,7 +1226,7 @@ def test_request_extra_json(self) -> None: assert data == {"baz": False} # `extra_json` takes priority over `json_data` when keys clash - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1205,8 +1237,8 @@ def test_request_extra_json(self) -> None: data = json.loads(request.content.decode("utf-8")) assert data == {"foo": "bar", "baz": None} - def test_request_extra_headers(self) -> None: - request = self.client._build_request( + def test_request_extra_headers(self, client: Kernel) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1216,7 +1248,7 @@ def test_request_extra_headers(self) -> None: assert request.headers.get("X-Foo") == "Foo" # `extra_headers` takes priority over `default_headers` when keys clash - request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request( + request = client.with_options(default_headers={"X-Bar": "true"})._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1227,8 +1259,8 @@ def test_request_extra_headers(self) -> None: ) assert request.headers.get("X-Bar") == "false" - def test_request_extra_query(self) -> None: - request = self.client._build_request( + def test_request_extra_query(self, client: Kernel) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1241,7 +1273,7 @@ def test_request_extra_query(self) -> None: assert params == {"my_query_param": "Foo"} # if both `query` and `extra_query` are given, they are merged - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1255,7 +1287,7 @@ def test_request_extra_query(self) -> None: assert params == {"bar": "1", "foo": "2"} # `extra_query` takes priority over `query` when keys clash - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1298,7 +1330,7 @@ def test_multipart_repeating_array(self, async_client: AsyncKernel) -> None: ] @pytest.mark.respx(base_url=base_url) - async def test_basic_union_response(self, respx_mock: MockRouter) -> None: + async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncKernel) -> None: class Model1(BaseModel): name: str @@ -1307,12 +1339,12 @@ class Model2(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) assert isinstance(response, Model2) assert response.foo == "bar" @pytest.mark.respx(base_url=base_url) - async def test_union_response_different_types(self, respx_mock: MockRouter) -> None: + async def test_union_response_different_types(self, respx_mock: MockRouter, async_client: AsyncKernel) -> None: """Union of objects with the same field name using a different type""" class Model1(BaseModel): @@ -1323,18 +1355,20 @@ class Model2(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) assert isinstance(response, Model2) assert response.foo == "bar" respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1})) - response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) assert isinstance(response, Model1) assert response.foo == 1 @pytest.mark.respx(base_url=base_url) - async def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None: + async def test_non_application_json_content_type_for_json_data( + self, respx_mock: MockRouter, async_client: AsyncKernel + ) -> None: """ Response that sets Content-Type to something other than application/json but returns json data """ @@ -1350,11 +1384,11 @@ class Model(BaseModel): ) ) - response = await self.client.get("/foo", cast_to=Model) + response = await async_client.get("/foo", cast_to=Model) assert isinstance(response, Model) assert response.foo == 2 - def test_base_url_setter(self) -> None: + async def test_base_url_setter(self) -> None: client = AsyncKernel( base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True ) @@ -1364,7 +1398,9 @@ def test_base_url_setter(self) -> None: assert client.base_url == "https://example.com/from_setter/" - def test_base_url_env(self) -> None: + await client.close() + + async def test_base_url_env(self) -> None: with update_env(KERNEL_BASE_URL="http://localhost:5000/from/env"): client = AsyncKernel(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" @@ -1379,6 +1415,8 @@ def test_base_url_env(self) -> None: ) assert str(client.base_url).startswith("https://api.onkernel.com/") + await client.close() + @pytest.mark.parametrize( "client", [ @@ -1394,7 +1432,7 @@ def test_base_url_env(self) -> None: ], ids=["standard", "custom http client"], ) - def test_base_url_trailing_slash(self, client: AsyncKernel) -> None: + async def test_base_url_trailing_slash(self, client: AsyncKernel) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1403,6 +1441,7 @@ def test_base_url_trailing_slash(self, client: AsyncKernel) -> None: ), ) assert request.url == "http://localhost:5000/custom/path/foo" + await client.close() @pytest.mark.parametrize( "client", @@ -1419,7 +1458,7 @@ def test_base_url_trailing_slash(self, client: AsyncKernel) -> None: ], ids=["standard", "custom http client"], ) - def test_base_url_no_trailing_slash(self, client: AsyncKernel) -> None: + async def test_base_url_no_trailing_slash(self, client: AsyncKernel) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1428,6 +1467,7 @@ def test_base_url_no_trailing_slash(self, client: AsyncKernel) -> None: ), ) assert request.url == "http://localhost:5000/custom/path/foo" + await client.close() @pytest.mark.parametrize( "client", @@ -1444,7 +1484,7 @@ def test_base_url_no_trailing_slash(self, client: AsyncKernel) -> None: ], ids=["standard", "custom http client"], ) - def test_absolute_request_url(self, client: AsyncKernel) -> None: + async def test_absolute_request_url(self, client: AsyncKernel) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1453,37 +1493,37 @@ def test_absolute_request_url(self, client: AsyncKernel) -> None: ), ) assert request.url == "https://myapi.com/foo" + await client.close() async def test_copied_client_does_not_close_http(self) -> None: - client = AsyncKernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) - assert not client.is_closed() + test_client = AsyncKernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) + assert not test_client.is_closed() - copied = client.copy() - assert copied is not client + copied = test_client.copy() + assert copied is not test_client del copied await asyncio.sleep(0.2) - assert not client.is_closed() + assert not test_client.is_closed() async def test_client_context_manager(self) -> None: - client = AsyncKernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) - async with client as c2: - assert c2 is client + test_client = AsyncKernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) + async with test_client as c2: + assert c2 is test_client assert not c2.is_closed() - assert not client.is_closed() - assert client.is_closed() + assert not test_client.is_closed() + assert test_client.is_closed() @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio - async def test_client_response_validation_error(self, respx_mock: MockRouter) -> None: + async def test_client_response_validation_error(self, respx_mock: MockRouter, async_client: AsyncKernel) -> None: class Model(BaseModel): foo: str respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}})) with pytest.raises(APIResponseValidationError) as exc: - await self.client.get("/foo", cast_to=Model) + await async_client.get("/foo", cast_to=Model) assert isinstance(exc.value.__cause__, ValidationError) @@ -1494,7 +1534,6 @@ async def test_client_max_retries_validation(self) -> None: ) @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio async def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: class Model(BaseModel): name: str @@ -1506,11 +1545,14 @@ class Model(BaseModel): with pytest.raises(APIResponseValidationError): await strict_client.get("/foo", cast_to=Model) - client = AsyncKernel(base_url=base_url, api_key=api_key, _strict_response_validation=False) + non_strict_client = AsyncKernel(base_url=base_url, api_key=api_key, _strict_response_validation=False) - response = await client.get("/foo", cast_to=Model) + response = await non_strict_client.get("/foo", cast_to=Model) assert isinstance(response, str) # type: ignore[unreachable] + await strict_client.close() + await non_strict_client.close() + @pytest.mark.parametrize( "remaining_retries,retry_after,timeout", [ @@ -1533,13 +1575,12 @@ class Model(BaseModel): ], ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) - @pytest.mark.asyncio - async def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None: - client = AsyncKernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) - + async def test_parse_retry_after_header( + self, remaining_retries: int, retry_after: str, timeout: float, async_client: AsyncKernel + ) -> None: headers = httpx.Headers({"retry-after": retry_after}) options = FinalRequestOptions(method="get", url="/foo", max_retries=3) - calculated = client._calculate_retry_timeout(remaining_retries, options, headers) + calculated = async_client._calculate_retry_timeout(remaining_retries, options, headers) assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] @mock.patch("kernel._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @@ -1550,7 +1591,7 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, with pytest.raises(APITimeoutError): await async_client.browsers.with_streaming_response.create().__aenter__() - assert _get_open_connections(self.client) == 0 + assert _get_open_connections(async_client) == 0 @mock.patch("kernel._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) @@ -1559,12 +1600,11 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, with pytest.raises(APIStatusError): await async_client.browsers.with_streaming_response.create().__aenter__() - assert _get_open_connections(self.client) == 0 + assert _get_open_connections(async_client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @mock.patch("kernel._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, @@ -1596,7 +1636,6 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @mock.patch("kernel._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: AsyncKernel, failures_before_success: int, respx_mock: MockRouter ) -> None: @@ -1620,7 +1659,6 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @mock.patch("kernel._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: AsyncKernel, failures_before_success: int, respx_mock: MockRouter ) -> None: @@ -1668,26 +1706,26 @@ async def test_default_client_creation(self) -> None: ) @pytest.mark.respx(base_url=base_url) - async def test_follow_redirects(self, respx_mock: MockRouter) -> None: + async def test_follow_redirects(self, respx_mock: MockRouter, async_client: AsyncKernel) -> None: # Test that the default follow_redirects=True allows following redirects respx_mock.post("/redirect").mock( return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) ) respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"})) - response = await self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) + response = await async_client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) assert response.status_code == 200 assert response.json() == {"status": "ok"} @pytest.mark.respx(base_url=base_url) - async def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None: + async def test_follow_redirects_disabled(self, respx_mock: MockRouter, async_client: AsyncKernel) -> None: # Test that follow_redirects=False prevents following redirects respx_mock.post("/redirect").mock( return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) ) with pytest.raises(APIStatusError) as exc_info: - await self.client.post( + await async_client.post( "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response ) From 147a0c1bb749f9838fa993a3474b313dabc344f7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 03:51:53 +0000 Subject: [PATCH 204/448] chore(internal): grammar fix (it's -> its) --- src/kernel/_utils/_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kernel/_utils/_utils.py b/src/kernel/_utils/_utils.py index 50d59269..eec7f4a1 100644 --- a/src/kernel/_utils/_utils.py +++ b/src/kernel/_utils/_utils.py @@ -133,7 +133,7 @@ def is_given(obj: _T | NotGiven | Omit) -> TypeGuard[_T]: # Type safe methods for narrowing types with TypeVars. # The default narrowing for isinstance(obj, dict) is dict[unknown, unknown], # however this cause Pyright to rightfully report errors. As we know we don't -# care about the contained types we can safely use `object` in it's place. +# care about the contained types we can safely use `object` in its place. # # There are two separate functions defined, `is_*` and `is_*_t` for different use cases. # `is_*` is for when you're dealing with an unknown input From f1b13857fc2dc4be7b1ce5e54fed5b545ee998e1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 7 Nov 2025 21:17:51 +0000 Subject: [PATCH 205/448] feat: Remove price gating on computer endpoints --- .stats.yml | 4 ++-- src/kernel/resources/apps.py | 8 ++++---- src/kernel/types/app_list_params.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.stats.yml b/.stats.yml index 9e4dbe62..0080fd62 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 65 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-015c11efc34c81d4d82a937c017f5eb789ea3ca21a05b70e2ed31c069b839293.yml -openapi_spec_hash: 3dcab2044da305f376cecf4eca38caee +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-8c7e0b9069a18bc9437269618cde251ba15568771f2b4811d57f0d5f0fd5692d.yml +openapi_spec_hash: aa2544d0bf0e7e875939aaa8e2e114d3 config_hash: 0fbdda3a736cc2748ca33371871e61b3 diff --git a/src/kernel/resources/apps.py b/src/kernel/resources/apps.py index 34aa1299..b803299d 100644 --- a/src/kernel/resources/apps.py +++ b/src/kernel/resources/apps.py @@ -63,9 +63,9 @@ def list( Args: app_name: Filter results by application name. - limit: Limit the number of app to return. + limit: Limit the number of apps to return. - offset: Offset the number of app to return. + offset: Offset the number of apps to return. version: Filter results by version label. @@ -140,9 +140,9 @@ def list( Args: app_name: Filter results by application name. - limit: Limit the number of app to return. + limit: Limit the number of apps to return. - offset: Offset the number of app to return. + offset: Offset the number of apps to return. version: Filter results by version label. diff --git a/src/kernel/types/app_list_params.py b/src/kernel/types/app_list_params.py index 1e2f5278..296ded55 100644 --- a/src/kernel/types/app_list_params.py +++ b/src/kernel/types/app_list_params.py @@ -12,10 +12,10 @@ class AppListParams(TypedDict, total=False): """Filter results by application name.""" limit: int - """Limit the number of app to return.""" + """Limit the number of apps to return.""" offset: int - """Offset the number of app to return.""" + """Offset the number of apps to return.""" version: str """Filter results by version label.""" From d87cb4bfe91a6dd004e448c5922d1164c6bcf7b6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 03:46:02 +0000 Subject: [PATCH 206/448] chore(package): drop Python 3.8 support --- README.md | 4 ++-- pyproject.toml | 5 ++--- src/kernel/_utils/_sync.py | 34 +++------------------------------- 3 files changed, 7 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index ae0066ec..699f3c26 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![PyPI version](https://img.shields.io/pypi/v/kernel.svg?label=pypi%20(stable))](https://pypi.org/project/kernel/) -The Kernel Python library provides convenient access to the Kernel REST API from any Python 3.8+ +The Kernel Python library provides convenient access to the Kernel REST API from any Python 3.9+ application. The library includes type definitions for all request params and response fields, and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx). @@ -487,7 +487,7 @@ print(kernel.__version__) ## Requirements -Python 3.8 or higher. +Python 3.9 or higher. ## Contributing diff --git a/pyproject.toml b/pyproject.toml index 737af0ac..52ba59c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,11 +15,10 @@ dependencies = [ "distro>=1.7.0, <2", "sniffio", ] -requires-python = ">= 3.8" +requires-python = ">= 3.9" classifiers = [ "Typing :: Typed", "Intended Audience :: Developers", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -141,7 +140,7 @@ filterwarnings = [ # there are a couple of flags that are still disabled by # default in strict mode as they are experimental and niche. typeCheckingMode = "strict" -pythonVersion = "3.8" +pythonVersion = "3.9" exclude = [ "_dev", diff --git a/src/kernel/_utils/_sync.py b/src/kernel/_utils/_sync.py index ad7ec71b..f6027c18 100644 --- a/src/kernel/_utils/_sync.py +++ b/src/kernel/_utils/_sync.py @@ -1,10 +1,8 @@ from __future__ import annotations -import sys import asyncio import functools -import contextvars -from typing import Any, TypeVar, Callable, Awaitable +from typing import TypeVar, Callable, Awaitable from typing_extensions import ParamSpec import anyio @@ -15,34 +13,11 @@ T_ParamSpec = ParamSpec("T_ParamSpec") -if sys.version_info >= (3, 9): - _asyncio_to_thread = asyncio.to_thread -else: - # backport of https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread - # for Python 3.8 support - async def _asyncio_to_thread( - func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs - ) -> Any: - """Asynchronously run function *func* in a separate thread. - - Any *args and **kwargs supplied for this function are directly passed - to *func*. Also, the current :class:`contextvars.Context` is propagated, - allowing context variables from the main thread to be accessed in the - separate thread. - - Returns a coroutine that can be awaited to get the eventual result of *func*. - """ - loop = asyncio.events.get_running_loop() - ctx = contextvars.copy_context() - func_call = functools.partial(ctx.run, func, *args, **kwargs) - return await loop.run_in_executor(None, func_call) - - async def to_thread( func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs ) -> T_Retval: if sniffio.current_async_library() == "asyncio": - return await _asyncio_to_thread(func, *args, **kwargs) + return await asyncio.to_thread(func, *args, **kwargs) return await anyio.to_thread.run_sync( functools.partial(func, *args, **kwargs), @@ -53,10 +28,7 @@ async def to_thread( def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]: """ Take a blocking function and create an async one that receives the same - positional and keyword arguments. For python version 3.9 and above, it uses - asyncio.to_thread to run the function in a separate thread. For python version - 3.8, it uses locally defined copy of the asyncio.to_thread function which was - introduced in python 3.9. + positional and keyword arguments. Usage: From 0f051b96483446fa2d781dbb3f07618c844ee05f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 03:46:48 +0000 Subject: [PATCH 207/448] fix: compat with Python 3.14 --- src/kernel/_models.py | 11 ++++++++--- tests/test_models.py | 8 ++++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/kernel/_models.py b/src/kernel/_models.py index 6a3cd1d2..fcec2cf9 100644 --- a/src/kernel/_models.py +++ b/src/kernel/_models.py @@ -2,6 +2,7 @@ import os import inspect +import weakref from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast from datetime import date, datetime from typing_extensions import ( @@ -573,6 +574,9 @@ class CachedDiscriminatorType(Protocol): __discriminator__: DiscriminatorDetails +DISCRIMINATOR_CACHE: weakref.WeakKeyDictionary[type, DiscriminatorDetails] = weakref.WeakKeyDictionary() + + class DiscriminatorDetails: field_name: str """The name of the discriminator field in the variant class, e.g. @@ -615,8 +619,9 @@ def __init__( def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, ...]) -> DiscriminatorDetails | None: - if isinstance(union, CachedDiscriminatorType): - return union.__discriminator__ + cached = DISCRIMINATOR_CACHE.get(union) + if cached is not None: + return cached discriminator_field_name: str | None = None @@ -669,7 +674,7 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, discriminator_field=discriminator_field_name, discriminator_alias=discriminator_alias, ) - cast(CachedDiscriminatorType, union).__discriminator__ = details + DISCRIMINATOR_CACHE.setdefault(union, details) return details diff --git a/tests/test_models.py b/tests/test_models.py index ff5955d4..78f0fd32 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -9,7 +9,7 @@ from kernel._utils import PropertyInfo from kernel._compat import PYDANTIC_V1, parse_obj, model_dump, model_json -from kernel._models import BaseModel, construct_type +from kernel._models import DISCRIMINATOR_CACHE, BaseModel, construct_type class BasicModel(BaseModel): @@ -809,7 +809,7 @@ class B(BaseModel): UnionType = cast(Any, Union[A, B]) - assert not hasattr(UnionType, "__discriminator__") + assert not DISCRIMINATOR_CACHE.get(UnionType) m = construct_type( value={"type": "b", "data": "foo"}, type_=cast(Any, Annotated[UnionType, PropertyInfo(discriminator="type")]) @@ -818,7 +818,7 @@ class B(BaseModel): assert m.type == "b" assert m.data == "foo" # type: ignore[comparison-overlap] - discriminator = UnionType.__discriminator__ + discriminator = DISCRIMINATOR_CACHE.get(UnionType) assert discriminator is not None m = construct_type( @@ -830,7 +830,7 @@ class B(BaseModel): # if the discriminator details object stays the same between invocations then # we hit the cache - assert UnionType.__discriminator__ is discriminator + assert DISCRIMINATOR_CACHE.get(UnionType) is discriminator @pytest.mark.skipif(PYDANTIC_V1, reason="TypeAliasType is not supported in Pydantic v1") From caf747489602eee5db4f6b465a783c1aa4bbecc6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 12 Nov 2025 03:41:33 +0000 Subject: [PATCH 208/448] fix(compat): update signatures of `model_dump` and `model_dump_json` for Pydantic v1 --- src/kernel/_models.py | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/src/kernel/_models.py b/src/kernel/_models.py index fcec2cf9..ca9500b2 100644 --- a/src/kernel/_models.py +++ b/src/kernel/_models.py @@ -257,15 +257,16 @@ def model_dump( mode: Literal["json", "python"] | str = "python", include: IncEx | None = None, exclude: IncEx | None = None, + context: Any | None = None, by_alias: bool | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, + exclude_computed_fields: bool = False, round_trip: bool = False, warnings: bool | Literal["none", "warn", "error"] = True, - context: dict[str, Any] | None = None, - serialize_as_any: bool = False, fallback: Callable[[Any], Any] | None = None, + serialize_as_any: bool = False, ) -> dict[str, Any]: """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump @@ -273,16 +274,24 @@ def model_dump( Args: mode: The mode in which `to_python` should run. - If mode is 'json', the dictionary will only contain JSON serializable types. - If mode is 'python', the dictionary may contain any Python objects. - include: A list of fields to include in the output. - exclude: A list of fields to exclude from the output. + If mode is 'json', the output will only contain JSON serializable types. + If mode is 'python', the output may contain non-JSON-serializable Python objects. + include: A set of fields to include in the output. + exclude: A set of fields to exclude from the output. + context: Additional context to pass to the serializer. by_alias: Whether to use the field's alias in the dictionary key if defined. - exclude_unset: Whether to exclude fields that are unset or None from the output. - exclude_defaults: Whether to exclude fields that are set to their default value from the output. - exclude_none: Whether to exclude fields that have a value of `None` from the output. - round_trip: Whether to enable serialization and deserialization round-trip support. - warnings: Whether to log warnings when invalid fields are encountered. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that are set to their default value. + exclude_none: Whether to exclude fields that have a value of `None`. + exclude_computed_fields: Whether to exclude computed fields. + While this can be useful for round-tripping, it is usually recommended to use the dedicated + `round_trip` parameter instead. + round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T]. + warnings: How to handle serialization errors. False/"none" ignores them, True/"warn" logs errors, + "error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError]. + fallback: A function to call when an unknown value is encountered. If not provided, + a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised. + serialize_as_any: Whether to serialize fields with duck-typing serialization behavior. Returns: A dictionary representation of the model. @@ -299,6 +308,8 @@ def model_dump( raise ValueError("serialize_as_any is only supported in Pydantic v2") if fallback is not None: raise ValueError("fallback is only supported in Pydantic v2") + if exclude_computed_fields != False: + raise ValueError("exclude_computed_fields is only supported in Pydantic v2") dumped = super().dict( # pyright: ignore[reportDeprecated] include=include, exclude=exclude, @@ -315,15 +326,17 @@ def model_dump_json( self, *, indent: int | None = None, + ensure_ascii: bool = False, include: IncEx | None = None, exclude: IncEx | None = None, + context: Any | None = None, by_alias: bool | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, + exclude_computed_fields: bool = False, round_trip: bool = False, warnings: bool | Literal["none", "warn", "error"] = True, - context: dict[str, Any] | None = None, fallback: Callable[[Any], Any] | None = None, serialize_as_any: bool = False, ) -> str: @@ -355,6 +368,10 @@ def model_dump_json( raise ValueError("serialize_as_any is only supported in Pydantic v2") if fallback is not None: raise ValueError("fallback is only supported in Pydantic v2") + if ensure_ascii != False: + raise ValueError("ensure_ascii is only supported in Pydantic v2") + if exclude_computed_fields != False: + raise ValueError("exclude_computed_fields is only supported in Pydantic v2") return super().json( # type: ignore[reportDeprecated] indent=indent, include=include, From e6060afab7affa99d1b9407f9dd51e06f32332ee Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 12 Nov 2025 16:43:47 +0000 Subject: [PATCH 209/448] feat: feat hide cursor v2 --- .stats.yml | 8 +- api.md | 7 ++ src/kernel/resources/browsers/computer.py | 92 ++++++++++++++++++ src/kernel/types/browsers/__init__.py | 6 ++ .../computer_set_cursor_visibility_params.py | 12 +++ ...computer_set_cursor_visibility_response.py | 10 ++ tests/api_resources/browsers/test_computer.py | 96 +++++++++++++++++++ 7 files changed, 227 insertions(+), 4 deletions(-) create mode 100644 src/kernel/types/browsers/computer_set_cursor_visibility_params.py create mode 100644 src/kernel/types/browsers/computer_set_cursor_visibility_response.py diff --git a/.stats.yml b/.stats.yml index 0080fd62..125a84b7 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 65 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-8c7e0b9069a18bc9437269618cde251ba15568771f2b4811d57f0d5f0fd5692d.yml -openapi_spec_hash: aa2544d0bf0e7e875939aaa8e2e114d3 -config_hash: 0fbdda3a736cc2748ca33371871e61b3 +configured_endpoints: 66 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-86854c41729a6b26f71e26c906f665f69939f23e2d7adcc43380aee64cf6d056.yml +openapi_spec_hash: 270a40c8af29e83cbda77d3700fd456a +config_hash: 9421eb86b7f3f4b274f123279da3858e diff --git a/api.md b/api.md index aa2ef0e8..8a015c3c 100644 --- a/api.md +++ b/api.md @@ -168,6 +168,12 @@ Methods: ## Computer +Types: + +```python +from kernel.types.browsers import ComputerSetCursorVisibilityResponse +``` + Methods: - client.browsers.computer.capture_screenshot(id, \*\*params) -> BinaryAPIResponse @@ -176,6 +182,7 @@ Methods: - client.browsers.computer.move_mouse(id, \*\*params) -> None - client.browsers.computer.press_key(id, \*\*params) -> None - client.browsers.computer.scroll(id, \*\*params) -> None +- client.browsers.computer.set_cursor_visibility(id, \*\*params) -> ComputerSetCursorVisibilityResponse - client.browsers.computer.type_text(id, \*\*params) -> None ## Playwright diff --git a/src/kernel/resources/browsers/computer.py b/src/kernel/resources/browsers/computer.py index 68cee420..87d377fd 100644 --- a/src/kernel/resources/browsers/computer.py +++ b/src/kernel/resources/browsers/computer.py @@ -34,7 +34,9 @@ computer_move_mouse_params, computer_click_mouse_params, computer_capture_screenshot_params, + computer_set_cursor_visibility_params, ) +from ...types.browsers.computer_set_cursor_visibility_response import ComputerSetCursorVisibilityResponse __all__ = ["ComputerResource", "AsyncComputerResource"] @@ -390,6 +392,45 @@ def scroll( cast_to=NoneType, ) + def set_cursor_visibility( + self, + id: str, + *, + hidden: bool, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via 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, + ) -> ComputerSetCursorVisibilityResponse: + """ + Set cursor visibility + + Args: + hidden: Whether the cursor should be hidden or visible + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/browsers/{id}/computer/cursor", + body=maybe_transform( + {"hidden": hidden}, computer_set_cursor_visibility_params.ComputerSetCursorVisibilityParams + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ComputerSetCursorVisibilityResponse, + ) + def type_text( self, id: str, @@ -789,6 +830,45 @@ async def scroll( cast_to=NoneType, ) + async def set_cursor_visibility( + self, + id: str, + *, + hidden: bool, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via 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, + ) -> ComputerSetCursorVisibilityResponse: + """ + Set cursor visibility + + Args: + hidden: Whether the cursor should be hidden or visible + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/browsers/{id}/computer/cursor", + body=await async_maybe_transform( + {"hidden": hidden}, computer_set_cursor_visibility_params.ComputerSetCursorVisibilityParams + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ComputerSetCursorVisibilityResponse, + ) + async def type_text( self, id: str, @@ -860,6 +940,9 @@ def __init__(self, computer: ComputerResource) -> None: self.scroll = to_raw_response_wrapper( computer.scroll, ) + self.set_cursor_visibility = to_raw_response_wrapper( + computer.set_cursor_visibility, + ) self.type_text = to_raw_response_wrapper( computer.type_text, ) @@ -888,6 +971,9 @@ def __init__(self, computer: AsyncComputerResource) -> None: self.scroll = async_to_raw_response_wrapper( computer.scroll, ) + self.set_cursor_visibility = async_to_raw_response_wrapper( + computer.set_cursor_visibility, + ) self.type_text = async_to_raw_response_wrapper( computer.type_text, ) @@ -916,6 +1002,9 @@ def __init__(self, computer: ComputerResource) -> None: self.scroll = to_streamed_response_wrapper( computer.scroll, ) + self.set_cursor_visibility = to_streamed_response_wrapper( + computer.set_cursor_visibility, + ) self.type_text = to_streamed_response_wrapper( computer.type_text, ) @@ -944,6 +1033,9 @@ def __init__(self, computer: AsyncComputerResource) -> None: self.scroll = async_to_streamed_response_wrapper( computer.scroll, ) + self.set_cursor_visibility = async_to_streamed_response_wrapper( + computer.set_cursor_visibility, + ) self.type_text = async_to_streamed_response_wrapper( computer.type_text, ) diff --git a/src/kernel/types/browsers/__init__.py b/src/kernel/types/browsers/__init__.py index f8a263c1..546fdc64 100644 --- a/src/kernel/types/browsers/__init__.py +++ b/src/kernel/types/browsers/__init__.py @@ -39,3 +39,9 @@ from .f_set_file_permissions_params import FSetFilePermissionsParams as FSetFilePermissionsParams from .process_stdout_stream_response import ProcessStdoutStreamResponse as ProcessStdoutStreamResponse from .computer_capture_screenshot_params import ComputerCaptureScreenshotParams as ComputerCaptureScreenshotParams +from .computer_set_cursor_visibility_params import ( + ComputerSetCursorVisibilityParams as ComputerSetCursorVisibilityParams, +) +from .computer_set_cursor_visibility_response import ( + ComputerSetCursorVisibilityResponse as ComputerSetCursorVisibilityResponse, +) diff --git a/src/kernel/types/browsers/computer_set_cursor_visibility_params.py b/src/kernel/types/browsers/computer_set_cursor_visibility_params.py new file mode 100644 index 00000000..f003ee91 --- /dev/null +++ b/src/kernel/types/browsers/computer_set_cursor_visibility_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["ComputerSetCursorVisibilityParams"] + + +class ComputerSetCursorVisibilityParams(TypedDict, total=False): + hidden: Required[bool] + """Whether the cursor should be hidden or visible""" diff --git a/src/kernel/types/browsers/computer_set_cursor_visibility_response.py b/src/kernel/types/browsers/computer_set_cursor_visibility_response.py new file mode 100644 index 00000000..c82302ef --- /dev/null +++ b/src/kernel/types/browsers/computer_set_cursor_visibility_response.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from ..._models import BaseModel + +__all__ = ["ComputerSetCursorVisibilityResponse"] + + +class ComputerSetCursorVisibilityResponse(BaseModel): + ok: bool + """Indicates success.""" diff --git a/tests/api_resources/browsers/test_computer.py b/tests/api_resources/browsers/test_computer.py index 9e245481..7634b89b 100644 --- a/tests/api_resources/browsers/test_computer.py +++ b/tests/api_resources/browsers/test_computer.py @@ -10,12 +10,16 @@ from respx import MockRouter from kernel import Kernel, AsyncKernel +from tests.utils import assert_matches_type from kernel._response import ( BinaryAPIResponse, AsyncBinaryAPIResponse, StreamedBinaryAPIResponse, AsyncStreamedBinaryAPIResponse, ) +from kernel.types.browsers import ( + ComputerSetCursorVisibilityResponse, +) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -396,6 +400,52 @@ def test_path_params_scroll(self, client: Kernel) -> None: y=0, ) + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_set_cursor_visibility(self, client: Kernel) -> None: + computer = client.browsers.computer.set_cursor_visibility( + id="id", + hidden=True, + ) + assert_matches_type(ComputerSetCursorVisibilityResponse, computer, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_set_cursor_visibility(self, client: Kernel) -> None: + response = client.browsers.computer.with_raw_response.set_cursor_visibility( + id="id", + hidden=True, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + computer = response.parse() + assert_matches_type(ComputerSetCursorVisibilityResponse, computer, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_set_cursor_visibility(self, client: Kernel) -> None: + with client.browsers.computer.with_streaming_response.set_cursor_visibility( + id="id", + hidden=True, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + computer = response.parse() + assert_matches_type(ComputerSetCursorVisibilityResponse, computer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_set_cursor_visibility(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.computer.with_raw_response.set_cursor_visibility( + id="", + hidden=True, + ) + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_type_text(self, client: Kernel) -> None: @@ -835,6 +885,52 @@ async def test_path_params_scroll(self, async_client: AsyncKernel) -> None: y=0, ) + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_set_cursor_visibility(self, async_client: AsyncKernel) -> None: + computer = await async_client.browsers.computer.set_cursor_visibility( + id="id", + hidden=True, + ) + assert_matches_type(ComputerSetCursorVisibilityResponse, computer, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_set_cursor_visibility(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.computer.with_raw_response.set_cursor_visibility( + id="id", + hidden=True, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + computer = await response.parse() + assert_matches_type(ComputerSetCursorVisibilityResponse, computer, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_set_cursor_visibility(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.computer.with_streaming_response.set_cursor_visibility( + id="id", + hidden=True, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + computer = await response.parse() + assert_matches_type(ComputerSetCursorVisibilityResponse, computer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_set_cursor_visibility(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.computer.with_raw_response.set_cursor_visibility( + id="", + hidden=True, + ) + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_type_text(self, async_client: AsyncKernel) -> None: From b9467c80cd12357060a639a7907cf0c4f292ebc7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 12 Nov 2025 18:04:28 +0000 Subject: [PATCH 210/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 4ad3fef3..e7562934 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.18.0" + ".": "0.19.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 52ba59c2..518a22eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.18.0" +version = "0.19.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index abb53bdb..dea2694c 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.18.0" # x-release-please-version +__version__ = "0.19.0" # x-release-please-version From a6ff7360df6a312a0bc9eedb592cc0ef138b92b3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:09:09 +0000 Subject: [PATCH 211/448] feat: works locally --- .stats.yml | 4 ++-- src/kernel/resources/browsers/browsers.py | 8 ++++---- src/kernel/types/browser_create_params.py | 4 ++-- src/kernel/types/browser_create_response.py | 4 ++-- src/kernel/types/browser_list_response.py | 4 ++-- src/kernel/types/browser_retrieve_response.py | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.stats.yml b/.stats.yml index 125a84b7..b12b0715 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 66 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-86854c41729a6b26f71e26c906f665f69939f23e2d7adcc43380aee64cf6d056.yml -openapi_spec_hash: 270a40c8af29e83cbda77d3700fd456a +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-7897c6c3f33d12ebf6cb8b3694945169617631a52af8f5b393b77b1995ed0d72.yml +openapi_spec_hash: 1104c3ba0915f1708d7576345cafa9d0 config_hash: 9421eb86b7f3f4b274f123279da3858e diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 6a0129c0..305cc51b 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -175,8 +175,8 @@ def create( image defaults apply (commonly 1024x768@60). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1024x768@60 If refresh_rate is not provided, it will be - automatically determined from the width and height if they match a supported + 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will + be automatically determined from the width and height if they match a supported configuration exactly. Note: Higher resolutions may affect the responsiveness of live view browser @@ -483,8 +483,8 @@ async def create( image defaults apply (commonly 1024x768@60). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1024x768@60 If refresh_rate is not provided, it will be - automatically determined from the width and height if they match a supported + 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will + be automatically determined from the width and height if they match a supported configuration exactly. Note: Higher resolutions may affect the responsiveness of live view browser diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index 23c3bb81..4994f2af 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -70,8 +70,8 @@ class BrowserCreateParams(TypedDict, total=False): If omitted, image defaults apply (commonly 1024x768@60). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1024x768@60 If refresh_rate is not provided, it will be - automatically determined from the width and height if they match a supported + 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will + be automatically determined from the width and height if they match a supported configuration exactly. Note: Higher resolutions may affect the responsiveness of live view browser """ diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index bcc50450..26fef74d 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -67,8 +67,8 @@ class BrowserCreateResponse(BaseModel): If omitted, image defaults apply (commonly 1024x768@60). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1024x768@60 If refresh_rate is not provided, it will be - automatically determined from the width and height if they match a supported + 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will + be automatically determined from the width and height if they match a supported configuration exactly. Note: Higher resolutions may affect the responsiveness of live view browser """ diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index a1b332fe..72ab6f3b 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -68,8 +68,8 @@ class BrowserListResponseItem(BaseModel): If omitted, image defaults apply (commonly 1024x768@60). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1024x768@60 If refresh_rate is not provided, it will be - automatically determined from the width and height if they match a supported + 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will + be automatically determined from the width and height if they match a supported configuration exactly. Note: Higher resolutions may affect the responsiveness of live view browser """ diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index f233929c..4d575f06 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -67,8 +67,8 @@ class BrowserRetrieveResponse(BaseModel): If omitted, image defaults apply (commonly 1024x768@60). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1024x768@60 If refresh_rate is not provided, it will be - automatically determined from the width and height if they match a supported + 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will + be automatically determined from the width and height if they match a supported configuration exactly. Note: Higher resolutions may affect the responsiveness of live view browser """ From b6f462df371d3157f99d24b74c8ce7cfcb6f5b26 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:34:39 +0000 Subject: [PATCH 212/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e7562934..96dfab30 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.19.0" + ".": "0.19.1" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 518a22eb..0dede543 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.19.0" +version = "0.19.1" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index dea2694c..3a441bd3 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.19.0" # x-release-please-version +__version__ = "0.19.1" # x-release-please-version From 6ecae08d23f4b978a3042dad3c81cccea1d687ed Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:21:28 +0000 Subject: [PATCH 213/448] feat: Feat increase max timeout to 72h --- .stats.yml | 4 ++-- src/kernel/resources/browsers/browsers.py | 4 ++-- src/kernel/types/browser_create_params.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.stats.yml b/.stats.yml index b12b0715..53123c95 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 66 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-7897c6c3f33d12ebf6cb8b3694945169617631a52af8f5b393b77b1995ed0d72.yml -openapi_spec_hash: 1104c3ba0915f1708d7576345cafa9d0 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-d611cf8b0301a07123eab0e92498bea5ad69c5292b28aca1016c362cca0a0564.yml +openapi_spec_hash: 6d30f4ad9d61a7da8a75d543cf3d3d75 config_hash: 9421eb86b7f3f4b274f123279da3858e diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 305cc51b..8503736c 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -167,7 +167,7 @@ def create( timeout_seconds: The number of seconds of inactivity before the browser session is terminated. Only applicable to non-persistent browsers. Activity includes CDP connections and live view connections. Defaults to 60 seconds. Minimum allowed is 10 - seconds. Maximum allowed is 86400 (24 hours). We check for inactivity every 5 + seconds. Maximum allowed is 259200 (72 hours). We check for inactivity every 5 seconds, so the actual timeout behavior you will see is +/- 5 seconds around the specified value. @@ -475,7 +475,7 @@ async def create( timeout_seconds: The number of seconds of inactivity before the browser session is terminated. Only applicable to non-persistent browsers. Activity includes CDP connections and live view connections. Defaults to 60 seconds. Minimum allowed is 10 - seconds. Maximum allowed is 86400 (24 hours). We check for inactivity every 5 + seconds. Maximum allowed is 259200 (72 hours). We check for inactivity every 5 seconds, so the actual timeout behavior you will see is +/- 5 seconds around the specified value. diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index 4994f2af..1e54ce75 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -59,7 +59,7 @@ class BrowserCreateParams(TypedDict, total=False): Only applicable to non-persistent browsers. Activity includes CDP connections and live view connections. Defaults to 60 seconds. Minimum allowed is 10 - seconds. Maximum allowed is 86400 (24 hours). We check for inactivity every 5 + seconds. Maximum allowed is 259200 (72 hours). We check for inactivity every 5 seconds, so the actual timeout behavior you will see is +/- 5 seconds around the specified value. """ From 75cc3a6c5a9bd0c82e412f46792157a9e9674077 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:43:03 +0000 Subject: [PATCH 214/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 96dfab30..c18333e6 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.19.1" + ".": "0.19.2" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 0dede543..fc74c8a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.19.1" +version = "0.19.2" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 3a441bd3..67d51017 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.19.1" # x-release-please-version +__version__ = "0.19.2" # x-release-please-version From 443c7e67c5995824d1d988882037af6f8fc68424 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 19:37:20 +0000 Subject: [PATCH 215/448] feat: allow get browser to paginate soft deleted browsers --- .stats.yml | 4 +- api.md | 2 +- src/kernel/resources/browsers/browsers.py | 102 +++++++++++++++--- src/kernel/types/__init__.py | 1 + src/kernel/types/browser_create_response.py | 3 + src/kernel/types/browser_list_params.py | 21 ++++ src/kernel/types/browser_list_response.py | 17 ++- src/kernel/types/browser_retrieve_response.py | 3 + tests/api_resources/test_browsers.py | 33 ++++-- 9 files changed, 155 insertions(+), 31 deletions(-) create mode 100644 src/kernel/types/browser_list_params.py diff --git a/.stats.yml b/.stats.yml index 53123c95..11b820d4 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 66 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-d611cf8b0301a07123eab0e92498bea5ad69c5292b28aca1016c362cca0a0564.yml -openapi_spec_hash: 6d30f4ad9d61a7da8a75d543cf3d3d75 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2af1b468584cb44aa9babbbfb82bff4055614fbb5c815084a6b7dacc1cf1a822.yml +openapi_spec_hash: 891affa2849341ea01d62011125f7edc config_hash: 9421eb86b7f3f4b274f123279da3858e diff --git a/api.md b/api.md index 8a015c3c..afa0ddd4 100644 --- a/api.md +++ b/api.md @@ -79,7 +79,7 @@ Methods: - client.browsers.create(\*\*params) -> BrowserCreateResponse - client.browsers.retrieve(id) -> BrowserRetrieveResponse -- client.browsers.list() -> BrowserListResponse +- client.browsers.list(\*\*params) -> SyncOffsetPagination[BrowserListResponse] - client.browsers.delete(\*\*params) -> None - client.browsers.delete_by_id(id) -> None - client.browsers.load_extensions(id, \*\*params) -> None diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 8503736c..84a4fe48 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -22,7 +22,12 @@ FsResourceWithStreamingResponse, AsyncFsResourceWithStreamingResponse, ) -from ...types import browser_create_params, browser_delete_params, browser_load_extensions_params +from ...types import ( + browser_list_params, + browser_create_params, + browser_delete_params, + browser_load_extensions_params, +) from .process import ( ProcessResource, AsyncProcessResource, @@ -65,7 +70,8 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from ..._base_client import make_request_options +from ...pagination import SyncOffsetPagination, AsyncOffsetPagination +from ..._base_client import AsyncPaginator, make_request_options from ...types.browser_list_response import BrowserListResponse from ...types.browser_create_response import BrowserCreateResponse from ...types.browser_persistence_param import BrowserPersistenceParam @@ -247,20 +253,55 @@ def retrieve( def list( self, *, + include_deleted: bool | Omit = omit, + limit: int | Omit = omit, + offset: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> BrowserListResponse: - """List active browser sessions""" - return self._get( + ) -> SyncOffsetPagination[BrowserListResponse]: + """List all browser sessions with pagination support. + + Use include_deleted=true to + include soft-deleted sessions in the results. + + Args: + include_deleted: When true, includes soft-deleted browser sessions in the results alongside + active sessions. + + limit: Maximum number of results to return. Defaults to 20, maximum 100. + + offset: Number of results to skip. Defaults to 0. + + 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_api_list( "/browsers", + page=SyncOffsetPagination[BrowserListResponse], options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "include_deleted": include_deleted, + "limit": limit, + "offset": offset, + }, + browser_list_params.BrowserListParams, + ), ), - cast_to=BrowserListResponse, + model=BrowserListResponse, ) def delete( @@ -552,23 +593,58 @@ async def retrieve( cast_to=BrowserRetrieveResponse, ) - async def list( + def list( self, *, + include_deleted: bool | Omit = omit, + limit: int | Omit = omit, + offset: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> BrowserListResponse: - """List active browser sessions""" - return await self._get( + ) -> AsyncPaginator[BrowserListResponse, AsyncOffsetPagination[BrowserListResponse]]: + """List all browser sessions with pagination support. + + Use include_deleted=true to + include soft-deleted sessions in the results. + + Args: + include_deleted: When true, includes soft-deleted browser sessions in the results alongside + active sessions. + + limit: Maximum number of results to return. Defaults to 20, maximum 100. + + offset: Number of results to skip. Defaults to 0. + + 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_api_list( "/browsers", + page=AsyncOffsetPagination[BrowserListResponse], options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "include_deleted": include_deleted, + "limit": limit, + "offset": offset, + }, + browser_list_params.BrowserListParams, + ), ), - cast_to=BrowserListResponse, + model=BrowserListResponse, ) async def delete( diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 6b49cf7f..208a8bda 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -13,6 +13,7 @@ from .profile import Profile as Profile from .app_list_params import AppListParams as AppListParams from .app_list_response import AppListResponse as AppListResponse +from .browser_list_params import BrowserListParams as BrowserListParams from .browser_persistence import BrowserPersistence as BrowserPersistence from .proxy_create_params import ProxyCreateParams as ProxyCreateParams from .proxy_list_response import ProxyListResponse as ProxyListResponse diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index 26fef74d..21041eaa 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -49,6 +49,9 @@ class BrowserCreateResponse(BaseModel): Only available for non-headless browsers. """ + deleted_at: Optional[datetime] = None + """When the browser session was soft-deleted. Only present for deleted sessions.""" + kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" diff --git a/src/kernel/types/browser_list_params.py b/src/kernel/types/browser_list_params.py new file mode 100644 index 00000000..20837bef --- /dev/null +++ b/src/kernel/types/browser_list_params.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["BrowserListParams"] + + +class BrowserListParams(TypedDict, total=False): + include_deleted: bool + """ + When true, includes soft-deleted browser sessions in the results alongside + active sessions. + """ + + limit: int + """Maximum number of results to return. Defaults to 20, maximum 100.""" + + offset: int + """Number of results to skip. Defaults to 0.""" diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index 72ab6f3b..74978690 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -1,17 +1,16 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List, Optional +from typing import Optional from datetime import datetime -from typing_extensions import TypeAlias from .profile import Profile from .._models import BaseModel from .browser_persistence import BrowserPersistence -__all__ = ["BrowserListResponse", "BrowserListResponseItem", "BrowserListResponseItemViewport"] +__all__ = ["BrowserListResponse", "Viewport"] -class BrowserListResponseItemViewport(BaseModel): +class Viewport(BaseModel): height: int """Browser window height in pixels.""" @@ -25,7 +24,7 @@ class BrowserListResponseItemViewport(BaseModel): """ -class BrowserListResponseItem(BaseModel): +class BrowserListResponse(BaseModel): cdp_ws_url: str """Websocket URL for Chrome DevTools Protocol connections to the browser session""" @@ -50,6 +49,9 @@ class BrowserListResponseItem(BaseModel): Only available for non-headless browsers. """ + deleted_at: Optional[datetime] = None + """When the browser session was soft-deleted. Only present for deleted sessions.""" + kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" @@ -62,7 +64,7 @@ class BrowserListResponseItem(BaseModel): proxy_id: Optional[str] = None """ID of the proxy associated with this browser session, if any.""" - viewport: Optional[BrowserListResponseItemViewport] = None + viewport: Optional[Viewport] = None """Initial browser window size in pixels with optional refresh rate. If omitted, image defaults apply (commonly 1024x768@60). Only specific viewport @@ -73,6 +75,3 @@ class BrowserListResponseItem(BaseModel): configuration exactly. Note: Higher resolutions may affect the responsiveness of live view browser """ - - -BrowserListResponse: TypeAlias = List[BrowserListResponseItem] diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 4d575f06..527386da 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -49,6 +49,9 @@ class BrowserRetrieveResponse(BaseModel): Only available for non-headless browsers. """ + deleted_at: Optional[datetime] = None + """When the browser session was soft-deleted. Only present for deleted sessions.""" + kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index bd75630e..c87fc3db 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -14,6 +14,7 @@ BrowserCreateResponse, BrowserRetrieveResponse, ) +from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -125,7 +126,17 @@ def test_path_params_retrieve(self, client: Kernel) -> None: @parametrize def test_method_list(self, client: Kernel) -> None: browser = client.browsers.list() - assert_matches_type(BrowserListResponse, browser, path=["response"]) + assert_matches_type(SyncOffsetPagination[BrowserListResponse], browser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_list_with_all_params(self, client: Kernel) -> None: + browser = client.browsers.list( + include_deleted=True, + limit=1, + offset=0, + ) + assert_matches_type(SyncOffsetPagination[BrowserListResponse], browser, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize @@ -135,7 +146,7 @@ def test_raw_response_list(self, client: Kernel) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" browser = response.parse() - assert_matches_type(BrowserListResponse, browser, path=["response"]) + assert_matches_type(SyncOffsetPagination[BrowserListResponse], browser, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize @@ -145,7 +156,7 @@ def test_streaming_response_list(self, client: Kernel) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" browser = response.parse() - assert_matches_type(BrowserListResponse, browser, path=["response"]) + assert_matches_type(SyncOffsetPagination[BrowserListResponse], browser, path=["response"]) assert cast(Any, response.is_closed) is True @@ -401,7 +412,17 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: @parametrize async def test_method_list(self, async_client: AsyncKernel) -> None: browser = await async_client.browsers.list() - assert_matches_type(BrowserListResponse, browser, path=["response"]) + assert_matches_type(AsyncOffsetPagination[BrowserListResponse], browser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: + browser = await async_client.browsers.list( + include_deleted=True, + limit=1, + offset=0, + ) + assert_matches_type(AsyncOffsetPagination[BrowserListResponse], browser, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize @@ -411,7 +432,7 @@ async def test_raw_response_list(self, async_client: AsyncKernel) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" browser = await response.parse() - assert_matches_type(BrowserListResponse, browser, path=["response"]) + assert_matches_type(AsyncOffsetPagination[BrowserListResponse], browser, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize @@ -421,7 +442,7 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" browser = await response.parse() - assert_matches_type(BrowserListResponse, browser, path=["response"]) + assert_matches_type(AsyncOffsetPagination[BrowserListResponse], browser, path=["response"]) assert cast(Any, response.is_closed) is True From 8f92d4ca3671931f002ad15ef778d8506a2c9011 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 19:55:52 +0000 Subject: [PATCH 216/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index c18333e6..0c2ecec6 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.19.2" + ".": "0.20.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index fc74c8a6..b5d3ecb5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.19.2" +version = "0.20.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 67d51017..1a28cba2 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.19.2" # x-release-please-version +__version__ = "0.20.0" # x-release-please-version From c7d7f8ad4f68efe8b57d7da450a4de4665d76e6f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 22 Nov 2025 03:34:11 +0000 Subject: [PATCH 217/448] chore: add Python 3.14 classifier and testing --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index b5d3ecb5..43139568 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: MacOS", From 08fa9c5597b2911cc7b431d37f4d8c6c77b3a27b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 18:52:40 +0000 Subject: [PATCH 218/448] feat: Mason/agent auth api --- .stats.yml | 8 +- api.md | 35 ++ src/kernel/_client.py | 9 + src/kernel/resources/__init__.py | 14 + src/kernel/resources/agents/__init__.py | 33 ++ src/kernel/resources/agents/agents.py | 102 ++++ src/kernel/resources/agents/auth/__init__.py | 33 ++ src/kernel/resources/agents/auth/auth.py | 239 ++++++++++ src/kernel/resources/agents/auth/runs.py | 434 ++++++++++++++++++ src/kernel/types/agents/__init__.py | 10 + .../agents/agent_auth_discover_response.py | 28 ++ .../types/agents/agent_auth_run_response.py | 22 + .../types/agents/agent_auth_start_response.py | 21 + .../agents/agent_auth_submit_response.py | 34 ++ src/kernel/types/agents/auth/__init__.py | 7 + .../types/agents/auth/run_exchange_params.py | 12 + .../agents/auth/run_exchange_response.py | 13 + .../types/agents/auth/run_submit_params.py | 13 + src/kernel/types/agents/auth_start_params.py | 26 ++ src/kernel/types/agents/discovered_field.py | 28 ++ tests/api_resources/agents/__init__.py | 1 + tests/api_resources/agents/auth/__init__.py | 1 + tests/api_resources/agents/auth/test_runs.py | 401 ++++++++++++++++ tests/api_resources/agents/test_auth.py | 120 +++++ 24 files changed, 1640 insertions(+), 4 deletions(-) create mode 100644 src/kernel/resources/agents/__init__.py create mode 100644 src/kernel/resources/agents/agents.py create mode 100644 src/kernel/resources/agents/auth/__init__.py create mode 100644 src/kernel/resources/agents/auth/auth.py create mode 100644 src/kernel/resources/agents/auth/runs.py create mode 100644 src/kernel/types/agents/__init__.py create mode 100644 src/kernel/types/agents/agent_auth_discover_response.py create mode 100644 src/kernel/types/agents/agent_auth_run_response.py create mode 100644 src/kernel/types/agents/agent_auth_start_response.py create mode 100644 src/kernel/types/agents/agent_auth_submit_response.py create mode 100644 src/kernel/types/agents/auth/__init__.py create mode 100644 src/kernel/types/agents/auth/run_exchange_params.py create mode 100644 src/kernel/types/agents/auth/run_exchange_response.py create mode 100644 src/kernel/types/agents/auth/run_submit_params.py create mode 100644 src/kernel/types/agents/auth_start_params.py create mode 100644 src/kernel/types/agents/discovered_field.py create mode 100644 tests/api_resources/agents/__init__.py create mode 100644 tests/api_resources/agents/auth/__init__.py create mode 100644 tests/api_resources/agents/auth/test_runs.py create mode 100644 tests/api_resources/agents/test_auth.py diff --git a/.stats.yml b/.stats.yml index 11b820d4..bdbede3e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 66 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2af1b468584cb44aa9babbbfb82bff4055614fbb5c815084a6b7dacc1cf1a822.yml -openapi_spec_hash: 891affa2849341ea01d62011125f7edc -config_hash: 9421eb86b7f3f4b274f123279da3858e +configured_endpoints: 71 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-bb3f37e55117a56e7a4208bd646d3a68adeb651ced8531e13fbfc1fc9dcb05a4.yml +openapi_spec_hash: 7303ce8ce3130e16a6a5c2bb49e43e9b +config_hash: be146470fb2d4583b6533859f0fa48f5 diff --git a/api.md b/api.md index afa0ddd4..483ff9cb 100644 --- a/api.md +++ b/api.md @@ -243,3 +243,38 @@ Methods: - client.extensions.download(id_or_name) -> BinaryAPIResponse - client.extensions.download_from_chrome_store(\*\*params) -> BinaryAPIResponse - client.extensions.upload(\*\*params) -> ExtensionUploadResponse + +# Agents + +## Auth + +Types: + +```python +from kernel.types.agents import ( + AgentAuthDiscoverResponse, + AgentAuthRunResponse, + AgentAuthStartResponse, + AgentAuthSubmitResponse, + DiscoveredField, +) +``` + +Methods: + +- client.agents.auth.start(\*\*params) -> AgentAuthStartResponse + +### Runs + +Types: + +```python +from kernel.types.agents.auth import RunExchangeResponse +``` + +Methods: + +- client.agents.auth.runs.retrieve(run_id) -> AgentAuthRunResponse +- client.agents.auth.runs.discover(run_id) -> AgentAuthDiscoverResponse +- client.agents.auth.runs.exchange(run_id, \*\*params) -> RunExchangeResponse +- client.agents.auth.runs.submit(run_id, \*\*params) -> AgentAuthSubmitResponse diff --git a/src/kernel/_client.py b/src/kernel/_client.py index ea9e51b9..ba424335 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -29,6 +29,7 @@ SyncAPIClient, AsyncAPIClient, ) +from .resources.agents import agents from .resources.browsers import browsers __all__ = [ @@ -57,6 +58,7 @@ class Kernel(SyncAPIClient): profiles: profiles.ProfilesResource proxies: proxies.ProxiesResource extensions: extensions.ExtensionsResource + agents: agents.AgentsResource with_raw_response: KernelWithRawResponse with_streaming_response: KernelWithStreamedResponse @@ -145,6 +147,7 @@ def __init__( self.profiles = profiles.ProfilesResource(self) self.proxies = proxies.ProxiesResource(self) self.extensions = extensions.ExtensionsResource(self) + self.agents = agents.AgentsResource(self) self.with_raw_response = KernelWithRawResponse(self) self.with_streaming_response = KernelWithStreamedResponse(self) @@ -263,6 +266,7 @@ class AsyncKernel(AsyncAPIClient): profiles: profiles.AsyncProfilesResource proxies: proxies.AsyncProxiesResource extensions: extensions.AsyncExtensionsResource + agents: agents.AsyncAgentsResource with_raw_response: AsyncKernelWithRawResponse with_streaming_response: AsyncKernelWithStreamedResponse @@ -351,6 +355,7 @@ def __init__( self.profiles = profiles.AsyncProfilesResource(self) self.proxies = proxies.AsyncProxiesResource(self) self.extensions = extensions.AsyncExtensionsResource(self) + self.agents = agents.AsyncAgentsResource(self) self.with_raw_response = AsyncKernelWithRawResponse(self) self.with_streaming_response = AsyncKernelWithStreamedResponse(self) @@ -470,6 +475,7 @@ def __init__(self, client: Kernel) -> None: self.profiles = profiles.ProfilesResourceWithRawResponse(client.profiles) self.proxies = proxies.ProxiesResourceWithRawResponse(client.proxies) self.extensions = extensions.ExtensionsResourceWithRawResponse(client.extensions) + self.agents = agents.AgentsResourceWithRawResponse(client.agents) class AsyncKernelWithRawResponse: @@ -481,6 +487,7 @@ def __init__(self, client: AsyncKernel) -> None: self.profiles = profiles.AsyncProfilesResourceWithRawResponse(client.profiles) self.proxies = proxies.AsyncProxiesResourceWithRawResponse(client.proxies) self.extensions = extensions.AsyncExtensionsResourceWithRawResponse(client.extensions) + self.agents = agents.AsyncAgentsResourceWithRawResponse(client.agents) class KernelWithStreamedResponse: @@ -492,6 +499,7 @@ def __init__(self, client: Kernel) -> None: self.profiles = profiles.ProfilesResourceWithStreamingResponse(client.profiles) self.proxies = proxies.ProxiesResourceWithStreamingResponse(client.proxies) self.extensions = extensions.ExtensionsResourceWithStreamingResponse(client.extensions) + self.agents = agents.AgentsResourceWithStreamingResponse(client.agents) class AsyncKernelWithStreamedResponse: @@ -503,6 +511,7 @@ def __init__(self, client: AsyncKernel) -> None: self.profiles = profiles.AsyncProfilesResourceWithStreamingResponse(client.profiles) self.proxies = proxies.AsyncProxiesResourceWithStreamingResponse(client.proxies) self.extensions = extensions.AsyncExtensionsResourceWithStreamingResponse(client.extensions) + self.agents = agents.AsyncAgentsResourceWithStreamingResponse(client.agents) Client = Kernel diff --git a/src/kernel/resources/__init__.py b/src/kernel/resources/__init__.py index 1b68d89f..233ef508 100644 --- a/src/kernel/resources/__init__.py +++ b/src/kernel/resources/__init__.py @@ -8,6 +8,14 @@ AppsResourceWithStreamingResponse, AsyncAppsResourceWithStreamingResponse, ) +from .agents import ( + AgentsResource, + AsyncAgentsResource, + AgentsResourceWithRawResponse, + AsyncAgentsResourceWithRawResponse, + AgentsResourceWithStreamingResponse, + AsyncAgentsResourceWithStreamingResponse, +) from .proxies import ( ProxiesResource, AsyncProxiesResource, @@ -100,4 +108,10 @@ "AsyncExtensionsResourceWithRawResponse", "ExtensionsResourceWithStreamingResponse", "AsyncExtensionsResourceWithStreamingResponse", + "AgentsResource", + "AsyncAgentsResource", + "AgentsResourceWithRawResponse", + "AsyncAgentsResourceWithRawResponse", + "AgentsResourceWithStreamingResponse", + "AsyncAgentsResourceWithStreamingResponse", ] diff --git a/src/kernel/resources/agents/__init__.py b/src/kernel/resources/agents/__init__.py new file mode 100644 index 00000000..cb159eb7 --- /dev/null +++ b/src/kernel/resources/agents/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .auth import ( + AuthResource, + AsyncAuthResource, + AuthResourceWithRawResponse, + AsyncAuthResourceWithRawResponse, + AuthResourceWithStreamingResponse, + AsyncAuthResourceWithStreamingResponse, +) +from .agents import ( + AgentsResource, + AsyncAgentsResource, + AgentsResourceWithRawResponse, + AsyncAgentsResourceWithRawResponse, + AgentsResourceWithStreamingResponse, + AsyncAgentsResourceWithStreamingResponse, +) + +__all__ = [ + "AuthResource", + "AsyncAuthResource", + "AuthResourceWithRawResponse", + "AsyncAuthResourceWithRawResponse", + "AuthResourceWithStreamingResponse", + "AsyncAuthResourceWithStreamingResponse", + "AgentsResource", + "AsyncAgentsResource", + "AgentsResourceWithRawResponse", + "AsyncAgentsResourceWithRawResponse", + "AgentsResourceWithStreamingResponse", + "AsyncAgentsResourceWithStreamingResponse", +] diff --git a/src/kernel/resources/agents/agents.py b/src/kernel/resources/agents/agents.py new file mode 100644 index 00000000..b7bb580c --- /dev/null +++ b/src/kernel/resources/agents/agents.py @@ -0,0 +1,102 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from ..._compat import cached_property +from .auth.auth import ( + AuthResource, + AsyncAuthResource, + AuthResourceWithRawResponse, + AsyncAuthResourceWithRawResponse, + AuthResourceWithStreamingResponse, + AsyncAuthResourceWithStreamingResponse, +) +from ..._resource import SyncAPIResource, AsyncAPIResource + +__all__ = ["AgentsResource", "AsyncAgentsResource"] + + +class AgentsResource(SyncAPIResource): + @cached_property + def auth(self) -> AuthResource: + return AuthResource(self._client) + + @cached_property + def with_raw_response(self) -> AgentsResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AgentsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AgentsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return AgentsResourceWithStreamingResponse(self) + + +class AsyncAgentsResource(AsyncAPIResource): + @cached_property + def auth(self) -> AsyncAuthResource: + return AsyncAuthResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncAgentsResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncAgentsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAgentsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return AsyncAgentsResourceWithStreamingResponse(self) + + +class AgentsResourceWithRawResponse: + def __init__(self, agents: AgentsResource) -> None: + self._agents = agents + + @cached_property + def auth(self) -> AuthResourceWithRawResponse: + return AuthResourceWithRawResponse(self._agents.auth) + + +class AsyncAgentsResourceWithRawResponse: + def __init__(self, agents: AsyncAgentsResource) -> None: + self._agents = agents + + @cached_property + def auth(self) -> AsyncAuthResourceWithRawResponse: + return AsyncAuthResourceWithRawResponse(self._agents.auth) + + +class AgentsResourceWithStreamingResponse: + def __init__(self, agents: AgentsResource) -> None: + self._agents = agents + + @cached_property + def auth(self) -> AuthResourceWithStreamingResponse: + return AuthResourceWithStreamingResponse(self._agents.auth) + + +class AsyncAgentsResourceWithStreamingResponse: + def __init__(self, agents: AsyncAgentsResource) -> None: + self._agents = agents + + @cached_property + def auth(self) -> AsyncAuthResourceWithStreamingResponse: + return AsyncAuthResourceWithStreamingResponse(self._agents.auth) diff --git a/src/kernel/resources/agents/auth/__init__.py b/src/kernel/resources/agents/auth/__init__.py new file mode 100644 index 00000000..d9853204 --- /dev/null +++ b/src/kernel/resources/agents/auth/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .auth import ( + AuthResource, + AsyncAuthResource, + AuthResourceWithRawResponse, + AsyncAuthResourceWithRawResponse, + AuthResourceWithStreamingResponse, + AsyncAuthResourceWithStreamingResponse, +) +from .runs import ( + RunsResource, + AsyncRunsResource, + RunsResourceWithRawResponse, + AsyncRunsResourceWithRawResponse, + RunsResourceWithStreamingResponse, + AsyncRunsResourceWithStreamingResponse, +) + +__all__ = [ + "RunsResource", + "AsyncRunsResource", + "RunsResourceWithRawResponse", + "AsyncRunsResourceWithRawResponse", + "RunsResourceWithStreamingResponse", + "AsyncRunsResourceWithStreamingResponse", + "AuthResource", + "AsyncAuthResource", + "AuthResourceWithRawResponse", + "AsyncAuthResourceWithRawResponse", + "AuthResourceWithStreamingResponse", + "AsyncAuthResourceWithStreamingResponse", +] diff --git a/src/kernel/resources/agents/auth/auth.py b/src/kernel/resources/agents/auth/auth.py new file mode 100644 index 00000000..2e099095 --- /dev/null +++ b/src/kernel/resources/agents/auth/auth.py @@ -0,0 +1,239 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from .runs import ( + RunsResource, + AsyncRunsResource, + RunsResourceWithRawResponse, + AsyncRunsResourceWithRawResponse, + RunsResourceWithStreamingResponse, + AsyncRunsResourceWithStreamingResponse, +) +from ...._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from ...._utils import maybe_transform, async_maybe_transform +from ...._compat import cached_property +from ...._resource import SyncAPIResource, AsyncAPIResource +from ...._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ...._base_client import make_request_options +from ....types.agents import auth_start_params +from ....types.agents.agent_auth_start_response import AgentAuthStartResponse + +__all__ = ["AuthResource", "AsyncAuthResource"] + + +class AuthResource(SyncAPIResource): + @cached_property + def runs(self) -> RunsResource: + return RunsResource(self._client) + + @cached_property + def with_raw_response(self) -> AuthResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AuthResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AuthResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return AuthResourceWithStreamingResponse(self) + + def start( + self, + *, + profile_name: str, + target_domain: str, + app_logo_url: str | Omit = omit, + proxy: auth_start_params.Proxy | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AgentAuthStartResponse: + """Creates a browser session and returns a handoff code for the hosted flow. + + Uses + standard API key or JWT authentication (not the JWT returned by the exchange + endpoint). + + Args: + profile_name: Name of the profile to use for this flow + + target_domain: Target domain for authentication + + app_logo_url: Optional logo URL for the application + + proxy: Optional proxy configuration + + 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( + "/agents/auth/start", + body=maybe_transform( + { + "profile_name": profile_name, + "target_domain": target_domain, + "app_logo_url": app_logo_url, + "proxy": proxy, + }, + auth_start_params.AuthStartParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AgentAuthStartResponse, + ) + + +class AsyncAuthResource(AsyncAPIResource): + @cached_property + def runs(self) -> AsyncRunsResource: + return AsyncRunsResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncAuthResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncAuthResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAuthResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return AsyncAuthResourceWithStreamingResponse(self) + + async def start( + self, + *, + profile_name: str, + target_domain: str, + app_logo_url: str | Omit = omit, + proxy: auth_start_params.Proxy | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AgentAuthStartResponse: + """Creates a browser session and returns a handoff code for the hosted flow. + + Uses + standard API key or JWT authentication (not the JWT returned by the exchange + endpoint). + + Args: + profile_name: Name of the profile to use for this flow + + target_domain: Target domain for authentication + + app_logo_url: Optional logo URL for the application + + proxy: Optional proxy configuration + + 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( + "/agents/auth/start", + body=await async_maybe_transform( + { + "profile_name": profile_name, + "target_domain": target_domain, + "app_logo_url": app_logo_url, + "proxy": proxy, + }, + auth_start_params.AuthStartParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AgentAuthStartResponse, + ) + + +class AuthResourceWithRawResponse: + def __init__(self, auth: AuthResource) -> None: + self._auth = auth + + self.start = to_raw_response_wrapper( + auth.start, + ) + + @cached_property + def runs(self) -> RunsResourceWithRawResponse: + return RunsResourceWithRawResponse(self._auth.runs) + + +class AsyncAuthResourceWithRawResponse: + def __init__(self, auth: AsyncAuthResource) -> None: + self._auth = auth + + self.start = async_to_raw_response_wrapper( + auth.start, + ) + + @cached_property + def runs(self) -> AsyncRunsResourceWithRawResponse: + return AsyncRunsResourceWithRawResponse(self._auth.runs) + + +class AuthResourceWithStreamingResponse: + def __init__(self, auth: AuthResource) -> None: + self._auth = auth + + self.start = to_streamed_response_wrapper( + auth.start, + ) + + @cached_property + def runs(self) -> RunsResourceWithStreamingResponse: + return RunsResourceWithStreamingResponse(self._auth.runs) + + +class AsyncAuthResourceWithStreamingResponse: + def __init__(self, auth: AsyncAuthResource) -> None: + self._auth = auth + + self.start = async_to_streamed_response_wrapper( + auth.start, + ) + + @cached_property + def runs(self) -> AsyncRunsResourceWithStreamingResponse: + return AsyncRunsResourceWithStreamingResponse(self._auth.runs) diff --git a/src/kernel/resources/agents/auth/runs.py b/src/kernel/resources/agents/auth/runs.py new file mode 100644 index 00000000..6ea09403 --- /dev/null +++ b/src/kernel/resources/agents/auth/runs.py @@ -0,0 +1,434 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict + +import httpx + +from ...._types import Body, Query, Headers, NotGiven, not_given +from ...._utils import maybe_transform, async_maybe_transform +from ...._compat import cached_property +from ...._resource import SyncAPIResource, AsyncAPIResource +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.agents.auth import run_submit_params, run_exchange_params +from ....types.agents.agent_auth_run_response import AgentAuthRunResponse +from ....types.agents.agent_auth_submit_response import AgentAuthSubmitResponse +from ....types.agents.auth.run_exchange_response import RunExchangeResponse +from ....types.agents.agent_auth_discover_response import AgentAuthDiscoverResponse + +__all__ = ["RunsResource", "AsyncRunsResource"] + + +class RunsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> RunsResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return RunsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> RunsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return RunsResourceWithStreamingResponse(self) + + def retrieve( + self, + run_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, + ) -> AgentAuthRunResponse: + """Returns run details including app_name and target_domain. + + Uses the JWT returned + by the exchange endpoint, or standard API key or JWT authentication. + + 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 run_id: + raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") + return self._get( + f"/agents/auth/runs/{run_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AgentAuthRunResponse, + ) + + def discover( + self, + run_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, + ) -> AgentAuthDiscoverResponse: + """ + Inspects the target site to detect logged-in state or discover required fields. + Returns 200 with success: true when fields are found, or 4xx/5xx for failures. + Requires the JWT returned by the exchange endpoint. + + 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 run_id: + raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") + return self._post( + f"/agents/auth/runs/{run_id}/discover", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AgentAuthDiscoverResponse, + ) + + def exchange( + self, + run_id: str, + *, + code: 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, + ) -> RunExchangeResponse: + """Validates the handoff code and returns a JWT token for subsequent requests. + + No + authentication required (the handoff code serves as the credential). + + Args: + code: Handoff code from start endpoint + + 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 run_id: + raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") + return self._post( + f"/agents/auth/runs/{run_id}/exchange", + body=maybe_transform({"code": code}, run_exchange_params.RunExchangeParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=RunExchangeResponse, + ) + + def submit( + self, + run_id: str, + *, + field_values: Dict[str, 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, + ) -> AgentAuthSubmitResponse: + """ + Submits field values for the discovered login form and may return additional + auth fields or success. Requires the JWT returned by the exchange endpoint. + + Args: + field_values: Values for the discovered login fields + + 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 run_id: + raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") + return self._post( + f"/agents/auth/runs/{run_id}/submit", + body=maybe_transform({"field_values": field_values}, run_submit_params.RunSubmitParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AgentAuthSubmitResponse, + ) + + +class AsyncRunsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncRunsResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncRunsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncRunsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return AsyncRunsResourceWithStreamingResponse(self) + + async def retrieve( + self, + run_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, + ) -> AgentAuthRunResponse: + """Returns run details including app_name and target_domain. + + Uses the JWT returned + by the exchange endpoint, or standard API key or JWT authentication. + + 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 run_id: + raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") + return await self._get( + f"/agents/auth/runs/{run_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AgentAuthRunResponse, + ) + + async def discover( + self, + run_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, + ) -> AgentAuthDiscoverResponse: + """ + Inspects the target site to detect logged-in state or discover required fields. + Returns 200 with success: true when fields are found, or 4xx/5xx for failures. + Requires the JWT returned by the exchange endpoint. + + 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 run_id: + raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") + return await self._post( + f"/agents/auth/runs/{run_id}/discover", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AgentAuthDiscoverResponse, + ) + + async def exchange( + self, + run_id: str, + *, + code: 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, + ) -> RunExchangeResponse: + """Validates the handoff code and returns a JWT token for subsequent requests. + + No + authentication required (the handoff code serves as the credential). + + Args: + code: Handoff code from start endpoint + + 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 run_id: + raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") + return await self._post( + f"/agents/auth/runs/{run_id}/exchange", + body=await async_maybe_transform({"code": code}, run_exchange_params.RunExchangeParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=RunExchangeResponse, + ) + + async def submit( + self, + run_id: str, + *, + field_values: Dict[str, 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, + ) -> AgentAuthSubmitResponse: + """ + Submits field values for the discovered login form and may return additional + auth fields or success. Requires the JWT returned by the exchange endpoint. + + Args: + field_values: Values for the discovered login fields + + 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 run_id: + raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") + return await self._post( + f"/agents/auth/runs/{run_id}/submit", + body=await async_maybe_transform({"field_values": field_values}, run_submit_params.RunSubmitParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AgentAuthSubmitResponse, + ) + + +class RunsResourceWithRawResponse: + def __init__(self, runs: RunsResource) -> None: + self._runs = runs + + self.retrieve = to_raw_response_wrapper( + runs.retrieve, + ) + self.discover = to_raw_response_wrapper( + runs.discover, + ) + self.exchange = to_raw_response_wrapper( + runs.exchange, + ) + self.submit = to_raw_response_wrapper( + runs.submit, + ) + + +class AsyncRunsResourceWithRawResponse: + def __init__(self, runs: AsyncRunsResource) -> None: + self._runs = runs + + self.retrieve = async_to_raw_response_wrapper( + runs.retrieve, + ) + self.discover = async_to_raw_response_wrapper( + runs.discover, + ) + self.exchange = async_to_raw_response_wrapper( + runs.exchange, + ) + self.submit = async_to_raw_response_wrapper( + runs.submit, + ) + + +class RunsResourceWithStreamingResponse: + def __init__(self, runs: RunsResource) -> None: + self._runs = runs + + self.retrieve = to_streamed_response_wrapper( + runs.retrieve, + ) + self.discover = to_streamed_response_wrapper( + runs.discover, + ) + self.exchange = to_streamed_response_wrapper( + runs.exchange, + ) + self.submit = to_streamed_response_wrapper( + runs.submit, + ) + + +class AsyncRunsResourceWithStreamingResponse: + def __init__(self, runs: AsyncRunsResource) -> None: + self._runs = runs + + self.retrieve = async_to_streamed_response_wrapper( + runs.retrieve, + ) + self.discover = async_to_streamed_response_wrapper( + runs.discover, + ) + self.exchange = async_to_streamed_response_wrapper( + runs.exchange, + ) + self.submit = async_to_streamed_response_wrapper( + runs.submit, + ) diff --git a/src/kernel/types/agents/__init__.py b/src/kernel/types/agents/__init__.py new file mode 100644 index 00000000..e8c22774 --- /dev/null +++ b/src/kernel/types/agents/__init__.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .discovered_field import DiscoveredField as DiscoveredField +from .auth_start_params import AuthStartParams as AuthStartParams +from .agent_auth_run_response import AgentAuthRunResponse as AgentAuthRunResponse +from .agent_auth_start_response import AgentAuthStartResponse as AgentAuthStartResponse +from .agent_auth_submit_response import AgentAuthSubmitResponse as AgentAuthSubmitResponse +from .agent_auth_discover_response import AgentAuthDiscoverResponse as AgentAuthDiscoverResponse diff --git a/src/kernel/types/agents/agent_auth_discover_response.py b/src/kernel/types/agents/agent_auth_discover_response.py new file mode 100644 index 00000000..000bdec2 --- /dev/null +++ b/src/kernel/types/agents/agent_auth_discover_response.py @@ -0,0 +1,28 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from ..._models import BaseModel +from .discovered_field import DiscoveredField + +__all__ = ["AgentAuthDiscoverResponse"] + + +class AgentAuthDiscoverResponse(BaseModel): + success: bool + """Whether discovery succeeded""" + + error_message: Optional[str] = None + """Error message if discovery failed""" + + fields: Optional[List[DiscoveredField]] = None + """Discovered form fields (present when success is true)""" + + logged_in: Optional[bool] = None + """Whether user is already logged in""" + + login_url: Optional[str] = None + """URL of the discovered login page""" + + page_title: Optional[str] = None + """Title of the login page""" diff --git a/src/kernel/types/agents/agent_auth_run_response.py b/src/kernel/types/agents/agent_auth_run_response.py new file mode 100644 index 00000000..0ec0b0bb --- /dev/null +++ b/src/kernel/types/agents/agent_auth_run_response.py @@ -0,0 +1,22 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from datetime import datetime +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["AgentAuthRunResponse"] + + +class AgentAuthRunResponse(BaseModel): + app_name: str + """App name (org name at time of run creation)""" + + expires_at: datetime + """When the handoff code expires""" + + status: Literal["ACTIVE", "ENDED", "EXPIRED", "CANCELED"] + """Run status""" + + target_domain: str + """Target domain for authentication""" diff --git a/src/kernel/types/agents/agent_auth_start_response.py b/src/kernel/types/agents/agent_auth_start_response.py new file mode 100644 index 00000000..2855fc2d --- /dev/null +++ b/src/kernel/types/agents/agent_auth_start_response.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from datetime import datetime + +from ..._models import BaseModel + +__all__ = ["AgentAuthStartResponse"] + + +class AgentAuthStartResponse(BaseModel): + expires_at: datetime + """When the handoff code expires""" + + handoff_code: str + """One-time code for handoff""" + + hosted_url: str + """URL to redirect user to""" + + run_id: str + """Unique identifier for the run""" diff --git a/src/kernel/types/agents/agent_auth_submit_response.py b/src/kernel/types/agents/agent_auth_submit_response.py new file mode 100644 index 00000000..c57002fb --- /dev/null +++ b/src/kernel/types/agents/agent_auth_submit_response.py @@ -0,0 +1,34 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from ..._models import BaseModel +from .discovered_field import DiscoveredField + +__all__ = ["AgentAuthSubmitResponse"] + + +class AgentAuthSubmitResponse(BaseModel): + success: bool + """Whether submission succeeded""" + + additional_fields: Optional[List[DiscoveredField]] = None + """ + Additional fields needed (e.g., OTP) - present when needs_additional_auth is + true + """ + + app_name: Optional[str] = None + """App name (only present when logged_in is true)""" + + error_message: Optional[str] = None + """Error message if submission failed""" + + logged_in: Optional[bool] = None + """Whether user is now logged in""" + + needs_additional_auth: Optional[bool] = None + """Whether additional authentication fields are needed""" + + target_domain: Optional[str] = None + """Target domain (only present when logged_in is true)""" diff --git a/src/kernel/types/agents/auth/__init__.py b/src/kernel/types/agents/auth/__init__.py new file mode 100644 index 00000000..78a13a38 --- /dev/null +++ b/src/kernel/types/agents/auth/__init__.py @@ -0,0 +1,7 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .run_submit_params import RunSubmitParams as RunSubmitParams +from .run_exchange_params import RunExchangeParams as RunExchangeParams +from .run_exchange_response import RunExchangeResponse as RunExchangeResponse diff --git a/src/kernel/types/agents/auth/run_exchange_params.py b/src/kernel/types/agents/auth/run_exchange_params.py new file mode 100644 index 00000000..1a23b25d --- /dev/null +++ b/src/kernel/types/agents/auth/run_exchange_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["RunExchangeParams"] + + +class RunExchangeParams(TypedDict, total=False): + code: Required[str] + """Handoff code from start endpoint""" diff --git a/src/kernel/types/agents/auth/run_exchange_response.py b/src/kernel/types/agents/auth/run_exchange_response.py new file mode 100644 index 00000000..347c57c3 --- /dev/null +++ b/src/kernel/types/agents/auth/run_exchange_response.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from ...._models import BaseModel + +__all__ = ["RunExchangeResponse"] + + +class RunExchangeResponse(BaseModel): + jwt: str + """JWT token with run_id claim (30 minute TTL)""" + + run_id: str + """Run ID""" diff --git a/src/kernel/types/agents/auth/run_submit_params.py b/src/kernel/types/agents/auth/run_submit_params.py new file mode 100644 index 00000000..efaf9ea5 --- /dev/null +++ b/src/kernel/types/agents/auth/run_submit_params.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict +from typing_extensions import Required, TypedDict + +__all__ = ["RunSubmitParams"] + + +class RunSubmitParams(TypedDict, total=False): + field_values: Required[Dict[str, str]] + """Values for the discovered login fields""" diff --git a/src/kernel/types/agents/auth_start_params.py b/src/kernel/types/agents/auth_start_params.py new file mode 100644 index 00000000..6e0f0c82 --- /dev/null +++ b/src/kernel/types/agents/auth_start_params.py @@ -0,0 +1,26 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["AuthStartParams", "Proxy"] + + +class AuthStartParams(TypedDict, total=False): + profile_name: Required[str] + """Name of the profile to use for this flow""" + + target_domain: Required[str] + """Target domain for authentication""" + + app_logo_url: str + """Optional logo URL for the application""" + + proxy: Proxy + """Optional proxy configuration""" + + +class Proxy(TypedDict, total=False): + proxy_id: str + """ID of the proxy to use""" diff --git a/src/kernel/types/agents/discovered_field.py b/src/kernel/types/agents/discovered_field.py new file mode 100644 index 00000000..90a4864c --- /dev/null +++ b/src/kernel/types/agents/discovered_field.py @@ -0,0 +1,28 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["DiscoveredField"] + + +class DiscoveredField(BaseModel): + label: str + """Field label""" + + name: str + """Field name""" + + selector: str + """CSS selector for the field""" + + type: Literal["text", "email", "password", "tel", "number", "url", "code", "checkbox"] + """Field type""" + + placeholder: Optional[str] = None + """Field placeholder""" + + required: Optional[bool] = None + """Whether field is required""" diff --git a/tests/api_resources/agents/__init__.py b/tests/api_resources/agents/__init__.py new file mode 100644 index 00000000..fd8019a9 --- /dev/null +++ b/tests/api_resources/agents/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/agents/auth/__init__.py b/tests/api_resources/agents/auth/__init__.py new file mode 100644 index 00000000..fd8019a9 --- /dev/null +++ b/tests/api_resources/agents/auth/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/agents/auth/test_runs.py b/tests/api_resources/agents/auth/test_runs.py new file mode 100644 index 00000000..25fbdb14 --- /dev/null +++ b/tests/api_resources/agents/auth/test_runs.py @@ -0,0 +1,401 @@ +# 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 kernel import Kernel, AsyncKernel +from tests.utils import assert_matches_type +from kernel.types.agents import AgentAuthRunResponse, AgentAuthSubmitResponse, AgentAuthDiscoverResponse +from kernel.types.agents.auth import RunExchangeResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestRuns: + 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: Kernel) -> None: + run = client.agents.auth.runs.retrieve( + "run_id", + ) + assert_matches_type(AgentAuthRunResponse, run, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: Kernel) -> None: + response = client.agents.auth.runs.with_raw_response.retrieve( + "run_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + run = response.parse() + assert_matches_type(AgentAuthRunResponse, run, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: Kernel) -> None: + with client.agents.auth.runs.with_streaming_response.retrieve( + "run_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + run = response.parse() + assert_matches_type(AgentAuthRunResponse, run, 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: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `run_id` but received ''"): + client.agents.auth.runs.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_discover(self, client: Kernel) -> None: + run = client.agents.auth.runs.discover( + "run_id", + ) + assert_matches_type(AgentAuthDiscoverResponse, run, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_discover(self, client: Kernel) -> None: + response = client.agents.auth.runs.with_raw_response.discover( + "run_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + run = response.parse() + assert_matches_type(AgentAuthDiscoverResponse, run, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_discover(self, client: Kernel) -> None: + with client.agents.auth.runs.with_streaming_response.discover( + "run_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + run = response.parse() + assert_matches_type(AgentAuthDiscoverResponse, run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_discover(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `run_id` but received ''"): + client.agents.auth.runs.with_raw_response.discover( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_exchange(self, client: Kernel) -> None: + run = client.agents.auth.runs.exchange( + run_id="run_id", + code="otp_abc123xyz", + ) + assert_matches_type(RunExchangeResponse, run, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_exchange(self, client: Kernel) -> None: + response = client.agents.auth.runs.with_raw_response.exchange( + run_id="run_id", + code="otp_abc123xyz", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + run = response.parse() + assert_matches_type(RunExchangeResponse, run, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_exchange(self, client: Kernel) -> None: + with client.agents.auth.runs.with_streaming_response.exchange( + run_id="run_id", + code="otp_abc123xyz", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + run = response.parse() + assert_matches_type(RunExchangeResponse, run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_exchange(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `run_id` but received ''"): + client.agents.auth.runs.with_raw_response.exchange( + run_id="", + code="otp_abc123xyz", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_submit(self, client: Kernel) -> None: + run = client.agents.auth.runs.submit( + run_id="run_id", + field_values={ + "email": "user@example.com", + "password": "********", + }, + ) + assert_matches_type(AgentAuthSubmitResponse, run, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_submit(self, client: Kernel) -> None: + response = client.agents.auth.runs.with_raw_response.submit( + run_id="run_id", + field_values={ + "email": "user@example.com", + "password": "********", + }, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + run = response.parse() + assert_matches_type(AgentAuthSubmitResponse, run, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_submit(self, client: Kernel) -> None: + with client.agents.auth.runs.with_streaming_response.submit( + run_id="run_id", + field_values={ + "email": "user@example.com", + "password": "********", + }, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + run = response.parse() + assert_matches_type(AgentAuthSubmitResponse, run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_submit(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `run_id` but received ''"): + client.agents.auth.runs.with_raw_response.submit( + run_id="", + field_values={ + "email": "user@example.com", + "password": "********", + }, + ) + + +class TestAsyncRuns: + 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: AsyncKernel) -> None: + run = await async_client.agents.auth.runs.retrieve( + "run_id", + ) + assert_matches_type(AgentAuthRunResponse, run, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: + response = await async_client.agents.auth.runs.with_raw_response.retrieve( + "run_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + run = await response.parse() + assert_matches_type(AgentAuthRunResponse, run, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: + async with async_client.agents.auth.runs.with_streaming_response.retrieve( + "run_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + run = await response.parse() + assert_matches_type(AgentAuthRunResponse, run, 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: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `run_id` but received ''"): + await async_client.agents.auth.runs.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_discover(self, async_client: AsyncKernel) -> None: + run = await async_client.agents.auth.runs.discover( + "run_id", + ) + assert_matches_type(AgentAuthDiscoverResponse, run, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_discover(self, async_client: AsyncKernel) -> None: + response = await async_client.agents.auth.runs.with_raw_response.discover( + "run_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + run = await response.parse() + assert_matches_type(AgentAuthDiscoverResponse, run, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_discover(self, async_client: AsyncKernel) -> None: + async with async_client.agents.auth.runs.with_streaming_response.discover( + "run_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + run = await response.parse() + assert_matches_type(AgentAuthDiscoverResponse, run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_discover(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `run_id` but received ''"): + await async_client.agents.auth.runs.with_raw_response.discover( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_exchange(self, async_client: AsyncKernel) -> None: + run = await async_client.agents.auth.runs.exchange( + run_id="run_id", + code="otp_abc123xyz", + ) + assert_matches_type(RunExchangeResponse, run, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_exchange(self, async_client: AsyncKernel) -> None: + response = await async_client.agents.auth.runs.with_raw_response.exchange( + run_id="run_id", + code="otp_abc123xyz", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + run = await response.parse() + assert_matches_type(RunExchangeResponse, run, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_exchange(self, async_client: AsyncKernel) -> None: + async with async_client.agents.auth.runs.with_streaming_response.exchange( + run_id="run_id", + code="otp_abc123xyz", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + run = await response.parse() + assert_matches_type(RunExchangeResponse, run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_exchange(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `run_id` but received ''"): + await async_client.agents.auth.runs.with_raw_response.exchange( + run_id="", + code="otp_abc123xyz", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_submit(self, async_client: AsyncKernel) -> None: + run = await async_client.agents.auth.runs.submit( + run_id="run_id", + field_values={ + "email": "user@example.com", + "password": "********", + }, + ) + assert_matches_type(AgentAuthSubmitResponse, run, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_submit(self, async_client: AsyncKernel) -> None: + response = await async_client.agents.auth.runs.with_raw_response.submit( + run_id="run_id", + field_values={ + "email": "user@example.com", + "password": "********", + }, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + run = await response.parse() + assert_matches_type(AgentAuthSubmitResponse, run, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_submit(self, async_client: AsyncKernel) -> None: + async with async_client.agents.auth.runs.with_streaming_response.submit( + run_id="run_id", + field_values={ + "email": "user@example.com", + "password": "********", + }, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + run = await response.parse() + assert_matches_type(AgentAuthSubmitResponse, run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_submit(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `run_id` but received ''"): + await async_client.agents.auth.runs.with_raw_response.submit( + run_id="", + field_values={ + "email": "user@example.com", + "password": "********", + }, + ) diff --git a/tests/api_resources/agents/test_auth.py b/tests/api_resources/agents/test_auth.py new file mode 100644 index 00000000..32d2784f --- /dev/null +++ b/tests/api_resources/agents/test_auth.py @@ -0,0 +1,120 @@ +# 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 kernel import Kernel, AsyncKernel +from tests.utils import assert_matches_type +from kernel.types.agents import AgentAuthStartResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestAuth: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_start(self, client: Kernel) -> None: + auth = client.agents.auth.start( + profile_name="auth-abc123", + target_domain="doordash.com", + ) + assert_matches_type(AgentAuthStartResponse, auth, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_start_with_all_params(self, client: Kernel) -> None: + auth = client.agents.auth.start( + profile_name="auth-abc123", + target_domain="doordash.com", + app_logo_url="https://example.com/logo.png", + proxy={"proxy_id": "proxy_id"}, + ) + assert_matches_type(AgentAuthStartResponse, auth, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_start(self, client: Kernel) -> None: + response = client.agents.auth.with_raw_response.start( + profile_name="auth-abc123", + target_domain="doordash.com", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + auth = response.parse() + assert_matches_type(AgentAuthStartResponse, auth, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_start(self, client: Kernel) -> None: + with client.agents.auth.with_streaming_response.start( + profile_name="auth-abc123", + target_domain="doordash.com", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + auth = response.parse() + assert_matches_type(AgentAuthStartResponse, auth, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncAuth: + 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_start(self, async_client: AsyncKernel) -> None: + auth = await async_client.agents.auth.start( + profile_name="auth-abc123", + target_domain="doordash.com", + ) + assert_matches_type(AgentAuthStartResponse, auth, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_start_with_all_params(self, async_client: AsyncKernel) -> None: + auth = await async_client.agents.auth.start( + profile_name="auth-abc123", + target_domain="doordash.com", + app_logo_url="https://example.com/logo.png", + proxy={"proxy_id": "proxy_id"}, + ) + assert_matches_type(AgentAuthStartResponse, auth, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_start(self, async_client: AsyncKernel) -> None: + response = await async_client.agents.auth.with_raw_response.start( + profile_name="auth-abc123", + target_domain="doordash.com", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + auth = await response.parse() + assert_matches_type(AgentAuthStartResponse, auth, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_start(self, async_client: AsyncKernel) -> None: + async with async_client.agents.auth.with_streaming_response.start( + profile_name="auth-abc123", + target_domain="doordash.com", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + auth = await response.parse() + assert_matches_type(AgentAuthStartResponse, auth, path=["response"]) + + assert cast(Any, response.is_closed) is True From bc52cf1229bd6de142877fc1e66a8d488e0684bd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 18:13:27 +0000 Subject: [PATCH 219/448] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index bdbede3e..fa4f9006 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 71 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-bb3f37e55117a56e7a4208bd646d3a68adeb651ced8531e13fbfc1fc9dcb05a4.yml -openapi_spec_hash: 7303ce8ce3130e16a6a5c2bb49e43e9b +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-ae9ed0d949aa701dd3873e49080fe923404a8869ffcb69b7c912a3f244d0236d.yml +openapi_spec_hash: 654d6e13a8bfe2103b373c668f43b33d config_hash: be146470fb2d4583b6533859f0fa48f5 From 0164b6f4c2cdf50d4e792e2b19ed5a27b299c595 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 00:27:26 +0000 Subject: [PATCH 220/448] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index fa4f9006..541095fa 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 71 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-ae9ed0d949aa701dd3873e49080fe923404a8869ffcb69b7c912a3f244d0236d.yml -openapi_spec_hash: 654d6e13a8bfe2103b373c668f43b33d +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2a63841293bec1bb651c5a24a95b2e9b5c07851dec1164de1aa2f87dafc51046.yml +openapi_spec_hash: d0bb3ca22c10b79758d503f717dd8e2f config_hash: be146470fb2d4583b6533859f0fa48f5 From 34663f1a74d9c3ebfe85434639573ce30e0a6b3d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 28 Nov 2025 03:12:20 +0000 Subject: [PATCH 221/448] fix: ensure streams are always closed --- src/kernel/_streaming.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/kernel/_streaming.py b/src/kernel/_streaming.py index e6d03062..369a3f6d 100644 --- a/src/kernel/_streaming.py +++ b/src/kernel/_streaming.py @@ -54,11 +54,12 @@ def __stream__(self) -> Iterator[_T]: process_data = self._client._process_response_data iterator = self._iter_events() - for sse in iterator: - yield process_data(data=sse.json(), cast_to=cast_to, response=response) - - # As we might not fully consume the response stream, we need to close it explicitly - response.close() + try: + for sse in iterator: + yield process_data(data=sse.json(), cast_to=cast_to, response=response) + finally: + # Ensure the response is closed even if the consumer doesn't read all data + response.close() def __enter__(self) -> Self: return self @@ -117,11 +118,12 @@ async def __stream__(self) -> AsyncIterator[_T]: process_data = self._client._process_response_data iterator = self._iter_events() - async for sse in iterator: - yield process_data(data=sse.json(), cast_to=cast_to, response=response) - - # As we might not fully consume the response stream, we need to close it explicitly - await response.aclose() + try: + async for sse in iterator: + yield process_data(data=sse.json(), cast_to=cast_to, response=response) + finally: + # Ensure the response is closed even if the consumer doesn't read all data + await response.aclose() async def __aenter__(self) -> Self: return self From 4863909d21145bdfa6e0f014d4f632b9e24e6825 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 28 Nov 2025 03:13:24 +0000 Subject: [PATCH 222/448] chore(deps): mypy 1.18.1 has a regression, pin to 1.17 --- pyproject.toml | 2 +- requirements-dev.lock | 4 +++- requirements.lock | 8 ++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 43139568..71c4c9aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ managed = true # version pins are in requirements-dev.lock dev-dependencies = [ "pyright==1.1.399", - "mypy", + "mypy==1.17", "respx", "pytest", "pytest-asyncio", diff --git a/requirements-dev.lock b/requirements-dev.lock index fc032738..b435fc70 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -72,7 +72,7 @@ mdurl==0.1.2 multidict==6.4.4 # via aiohttp # via yarl -mypy==1.14.1 +mypy==1.17.0 mypy-extensions==1.0.0 # via mypy nodeenv==1.8.0 @@ -81,6 +81,8 @@ nox==2023.4.22 packaging==23.2 # via nox # via pytest +pathspec==0.12.1 + # via mypy platformdirs==3.11.0 # via virtualenv pluggy==1.5.0 diff --git a/requirements.lock b/requirements.lock index ed64e3db..646e8cda 100644 --- a/requirements.lock +++ b/requirements.lock @@ -55,21 +55,21 @@ multidict==6.4.4 propcache==0.3.1 # via aiohttp # via yarl -pydantic==2.11.9 +pydantic==2.12.5 # via kernel -pydantic-core==2.33.2 +pydantic-core==2.41.5 # via pydantic sniffio==1.3.0 # via anyio # via kernel -typing-extensions==4.12.2 +typing-extensions==4.15.0 # via anyio # via kernel # via multidict # via pydantic # via pydantic-core # via typing-inspection -typing-inspection==0.4.1 +typing-inspection==0.4.2 # via pydantic yarl==1.20.0 # via aiohttp From e850f25e158f41e363ce7a0ad9e1e1c2ee781583 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 01:22:10 +0000 Subject: [PATCH 223/448] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 541095fa..a6b35ee5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 71 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2a63841293bec1bb651c5a24a95b2e9b5c07851dec1164de1aa2f87dafc51046.yml -openapi_spec_hash: d0bb3ca22c10b79758d503f717dd8e2f +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-92b20a9e4650f645d3bb23b64f4ae72287bb41d3922ff1371426a91879186362.yml +openapi_spec_hash: a3c5f41d36734c980bc5313ee60b97cf config_hash: be146470fb2d4583b6533859f0fa48f5 From d83df57cd16969fe3a840fbabce81f6ac3f79a43 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 20:24:28 +0000 Subject: [PATCH 224/448] feat: Browser pools sdk release --- .stats.yml | 8 +- api.md | 54 +- src/kernel/_client.py | 19 +- src/kernel/resources/__init__.py | 28 +- src/kernel/resources/agents/__init__.py | 33 - src/kernel/resources/agents/agents.py | 102 -- src/kernel/resources/agents/auth/__init__.py | 33 - src/kernel/resources/agents/auth/auth.py | 239 ---- src/kernel/resources/agents/auth/runs.py | 434 ------- src/kernel/resources/browser_pools.py | 1022 +++++++++++++++++ src/kernel/resources/browsers/browsers.py | 15 +- src/kernel/types/__init__.py | 12 + src/kernel/types/agents/__init__.py | 10 - .../agents/agent_auth_discover_response.py | 28 - .../types/agents/agent_auth_run_response.py | 22 - .../types/agents/agent_auth_start_response.py | 21 - .../agents/agent_auth_submit_response.py | 34 - src/kernel/types/agents/auth/__init__.py | 7 - .../types/agents/auth/run_exchange_params.py | 12 - .../agents/auth/run_exchange_response.py | 13 - .../types/agents/auth/run_submit_params.py | 13 - src/kernel/types/agents/auth_start_params.py | 26 - src/kernel/types/agents/discovered_field.py | 28 - src/kernel/types/browser_create_params.py | 55 +- src/kernel/types/browser_create_response.py | 19 +- src/kernel/types/browser_list_response.py | 19 +- src/kernel/types/browser_pool.py | 29 + .../types/browser_pool_acquire_params.py | 16 + .../types/browser_pool_acquire_response.py | 64 ++ .../types/browser_pool_create_params.py | 75 ++ .../types/browser_pool_delete_params.py | 15 + .../types/browser_pool_list_response.py | 10 + .../types/browser_pool_release_params.py | 18 + src/kernel/types/browser_pool_request.py | 73 ++ .../types/browser_pool_update_params.py | 81 ++ src/kernel/types/browser_retrieve_response.py | 19 +- src/kernel/types/shared/__init__.py | 3 + src/kernel/types/shared/browser_extension.py | 18 + src/kernel/types/shared/browser_profile.py | 24 + src/kernel/types/shared/browser_viewport.py | 21 + src/kernel/types/shared_params/__init__.py | 5 + .../types/shared_params/browser_extension.py | 18 + .../types/shared_params/browser_profile.py | 24 + .../types/shared_params/browser_viewport.py | 21 + tests/api_resources/agents/__init__.py | 1 - tests/api_resources/agents/auth/__init__.py | 1 - tests/api_resources/agents/auth/test_runs.py | 401 ------- tests/api_resources/agents/test_auth.py | 120 -- tests/api_resources/test_browser_pools.py | 856 ++++++++++++++ 49 files changed, 2486 insertions(+), 1733 deletions(-) delete mode 100644 src/kernel/resources/agents/__init__.py delete mode 100644 src/kernel/resources/agents/agents.py delete mode 100644 src/kernel/resources/agents/auth/__init__.py delete mode 100644 src/kernel/resources/agents/auth/auth.py delete mode 100644 src/kernel/resources/agents/auth/runs.py create mode 100644 src/kernel/resources/browser_pools.py delete mode 100644 src/kernel/types/agents/__init__.py delete mode 100644 src/kernel/types/agents/agent_auth_discover_response.py delete mode 100644 src/kernel/types/agents/agent_auth_run_response.py delete mode 100644 src/kernel/types/agents/agent_auth_start_response.py delete mode 100644 src/kernel/types/agents/agent_auth_submit_response.py delete mode 100644 src/kernel/types/agents/auth/__init__.py delete mode 100644 src/kernel/types/agents/auth/run_exchange_params.py delete mode 100644 src/kernel/types/agents/auth/run_exchange_response.py delete mode 100644 src/kernel/types/agents/auth/run_submit_params.py delete mode 100644 src/kernel/types/agents/auth_start_params.py delete mode 100644 src/kernel/types/agents/discovered_field.py create mode 100644 src/kernel/types/browser_pool.py create mode 100644 src/kernel/types/browser_pool_acquire_params.py create mode 100644 src/kernel/types/browser_pool_acquire_response.py create mode 100644 src/kernel/types/browser_pool_create_params.py create mode 100644 src/kernel/types/browser_pool_delete_params.py create mode 100644 src/kernel/types/browser_pool_list_response.py create mode 100644 src/kernel/types/browser_pool_release_params.py create mode 100644 src/kernel/types/browser_pool_request.py create mode 100644 src/kernel/types/browser_pool_update_params.py create mode 100644 src/kernel/types/shared/browser_extension.py create mode 100644 src/kernel/types/shared/browser_profile.py create mode 100644 src/kernel/types/shared/browser_viewport.py create mode 100644 src/kernel/types/shared_params/__init__.py create mode 100644 src/kernel/types/shared_params/browser_extension.py create mode 100644 src/kernel/types/shared_params/browser_profile.py create mode 100644 src/kernel/types/shared_params/browser_viewport.py delete mode 100644 tests/api_resources/agents/__init__.py delete mode 100644 tests/api_resources/agents/auth/__init__.py delete mode 100644 tests/api_resources/agents/auth/test_runs.py delete mode 100644 tests/api_resources/agents/test_auth.py create mode 100644 tests/api_resources/test_browser_pools.py diff --git a/.stats.yml b/.stats.yml index a6b35ee5..c32f96db 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 71 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-92b20a9e4650f645d3bb23b64f4ae72287bb41d3922ff1371426a91879186362.yml -openapi_spec_hash: a3c5f41d36734c980bc5313ee60b97cf -config_hash: be146470fb2d4583b6533859f0fa48f5 +configured_endpoints: 74 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-340c8f009b71922347d4c238c8715cd752c8965abfa12cbb1ffabe35edc338a8.yml +openapi_spec_hash: efc13ab03ef89cc07333db8ab5345f31 +config_hash: a4124701ae0a474e580d7416adbcfb00 diff --git a/api.md b/api.md index 483ff9cb..fe6b45c0 100644 --- a/api.md +++ b/api.md @@ -1,7 +1,17 @@ # Shared Types ```python -from kernel.types import AppAction, ErrorDetail, ErrorEvent, ErrorModel, HeartbeatEvent, LogEvent +from kernel.types import ( + AppAction, + BrowserExtension, + BrowserProfile, + BrowserViewport, + ErrorDetail, + ErrorEvent, + ErrorModel, + HeartbeatEvent, + LogEvent, +) ``` # Deployments @@ -244,37 +254,29 @@ Methods: - client.extensions.download_from_chrome_store(\*\*params) -> BinaryAPIResponse - client.extensions.upload(\*\*params) -> ExtensionUploadResponse -# Agents - -## Auth +# BrowserPools Types: ```python -from kernel.types.agents import ( - AgentAuthDiscoverResponse, - AgentAuthRunResponse, - AgentAuthStartResponse, - AgentAuthSubmitResponse, - DiscoveredField, +from kernel.types import ( + BrowserPool, + BrowserPoolAcquireRequest, + BrowserPoolReleaseRequest, + BrowserPoolRequest, + BrowserPoolUpdateRequest, + BrowserPoolListResponse, + BrowserPoolAcquireResponse, ) ``` Methods: -- client.agents.auth.start(\*\*params) -> AgentAuthStartResponse - -### Runs - -Types: - -```python -from kernel.types.agents.auth import RunExchangeResponse -``` - -Methods: - -- client.agents.auth.runs.retrieve(run_id) -> AgentAuthRunResponse -- client.agents.auth.runs.discover(run_id) -> AgentAuthDiscoverResponse -- client.agents.auth.runs.exchange(run_id, \*\*params) -> RunExchangeResponse -- client.agents.auth.runs.submit(run_id, \*\*params) -> AgentAuthSubmitResponse +- client.browser_pools.create(\*\*params) -> BrowserPool +- client.browser_pools.retrieve(id_or_name) -> BrowserPool +- client.browser_pools.update(id_or_name, \*\*params) -> BrowserPool +- client.browser_pools.list() -> BrowserPoolListResponse +- client.browser_pools.delete(id_or_name, \*\*params) -> None +- client.browser_pools.acquire(id_or_name, \*\*params) -> BrowserPoolAcquireResponse +- client.browser_pools.flush(id_or_name) -> None +- client.browser_pools.release(id_or_name, \*\*params) -> None diff --git a/src/kernel/_client.py b/src/kernel/_client.py index ba424335..37ba4890 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -21,7 +21,7 @@ ) from ._utils import is_given, get_async_library from ._version import __version__ -from .resources import apps, proxies, profiles, extensions, deployments, invocations +from .resources import apps, proxies, profiles, extensions, deployments, invocations, browser_pools from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import KernelError, APIStatusError from ._base_client import ( @@ -29,7 +29,6 @@ SyncAPIClient, AsyncAPIClient, ) -from .resources.agents import agents from .resources.browsers import browsers __all__ = [ @@ -58,7 +57,7 @@ class Kernel(SyncAPIClient): profiles: profiles.ProfilesResource proxies: proxies.ProxiesResource extensions: extensions.ExtensionsResource - agents: agents.AgentsResource + browser_pools: browser_pools.BrowserPoolsResource with_raw_response: KernelWithRawResponse with_streaming_response: KernelWithStreamedResponse @@ -147,7 +146,7 @@ def __init__( self.profiles = profiles.ProfilesResource(self) self.proxies = proxies.ProxiesResource(self) self.extensions = extensions.ExtensionsResource(self) - self.agents = agents.AgentsResource(self) + self.browser_pools = browser_pools.BrowserPoolsResource(self) self.with_raw_response = KernelWithRawResponse(self) self.with_streaming_response = KernelWithStreamedResponse(self) @@ -266,7 +265,7 @@ class AsyncKernel(AsyncAPIClient): profiles: profiles.AsyncProfilesResource proxies: proxies.AsyncProxiesResource extensions: extensions.AsyncExtensionsResource - agents: agents.AsyncAgentsResource + browser_pools: browser_pools.AsyncBrowserPoolsResource with_raw_response: AsyncKernelWithRawResponse with_streaming_response: AsyncKernelWithStreamedResponse @@ -355,7 +354,7 @@ def __init__( self.profiles = profiles.AsyncProfilesResource(self) self.proxies = proxies.AsyncProxiesResource(self) self.extensions = extensions.AsyncExtensionsResource(self) - self.agents = agents.AsyncAgentsResource(self) + self.browser_pools = browser_pools.AsyncBrowserPoolsResource(self) self.with_raw_response = AsyncKernelWithRawResponse(self) self.with_streaming_response = AsyncKernelWithStreamedResponse(self) @@ -475,7 +474,7 @@ def __init__(self, client: Kernel) -> None: self.profiles = profiles.ProfilesResourceWithRawResponse(client.profiles) self.proxies = proxies.ProxiesResourceWithRawResponse(client.proxies) self.extensions = extensions.ExtensionsResourceWithRawResponse(client.extensions) - self.agents = agents.AgentsResourceWithRawResponse(client.agents) + self.browser_pools = browser_pools.BrowserPoolsResourceWithRawResponse(client.browser_pools) class AsyncKernelWithRawResponse: @@ -487,7 +486,7 @@ def __init__(self, client: AsyncKernel) -> None: self.profiles = profiles.AsyncProfilesResourceWithRawResponse(client.profiles) self.proxies = proxies.AsyncProxiesResourceWithRawResponse(client.proxies) self.extensions = extensions.AsyncExtensionsResourceWithRawResponse(client.extensions) - self.agents = agents.AsyncAgentsResourceWithRawResponse(client.agents) + self.browser_pools = browser_pools.AsyncBrowserPoolsResourceWithRawResponse(client.browser_pools) class KernelWithStreamedResponse: @@ -499,7 +498,7 @@ def __init__(self, client: Kernel) -> None: self.profiles = profiles.ProfilesResourceWithStreamingResponse(client.profiles) self.proxies = proxies.ProxiesResourceWithStreamingResponse(client.proxies) self.extensions = extensions.ExtensionsResourceWithStreamingResponse(client.extensions) - self.agents = agents.AgentsResourceWithStreamingResponse(client.agents) + self.browser_pools = browser_pools.BrowserPoolsResourceWithStreamingResponse(client.browser_pools) class AsyncKernelWithStreamedResponse: @@ -511,7 +510,7 @@ def __init__(self, client: AsyncKernel) -> None: self.profiles = profiles.AsyncProfilesResourceWithStreamingResponse(client.profiles) self.proxies = proxies.AsyncProxiesResourceWithStreamingResponse(client.proxies) self.extensions = extensions.AsyncExtensionsResourceWithStreamingResponse(client.extensions) - self.agents = agents.AsyncAgentsResourceWithStreamingResponse(client.agents) + self.browser_pools = browser_pools.AsyncBrowserPoolsResourceWithStreamingResponse(client.browser_pools) Client = Kernel diff --git a/src/kernel/resources/__init__.py b/src/kernel/resources/__init__.py index 233ef508..cf08046e 100644 --- a/src/kernel/resources/__init__.py +++ b/src/kernel/resources/__init__.py @@ -8,14 +8,6 @@ AppsResourceWithStreamingResponse, AsyncAppsResourceWithStreamingResponse, ) -from .agents import ( - AgentsResource, - AsyncAgentsResource, - AgentsResourceWithRawResponse, - AsyncAgentsResourceWithRawResponse, - AgentsResourceWithStreamingResponse, - AsyncAgentsResourceWithStreamingResponse, -) from .proxies import ( ProxiesResource, AsyncProxiesResource, @@ -64,6 +56,14 @@ InvocationsResourceWithStreamingResponse, AsyncInvocationsResourceWithStreamingResponse, ) +from .browser_pools import ( + BrowserPoolsResource, + AsyncBrowserPoolsResource, + BrowserPoolsResourceWithRawResponse, + AsyncBrowserPoolsResourceWithRawResponse, + BrowserPoolsResourceWithStreamingResponse, + AsyncBrowserPoolsResourceWithStreamingResponse, +) __all__ = [ "DeploymentsResource", @@ -108,10 +108,10 @@ "AsyncExtensionsResourceWithRawResponse", "ExtensionsResourceWithStreamingResponse", "AsyncExtensionsResourceWithStreamingResponse", - "AgentsResource", - "AsyncAgentsResource", - "AgentsResourceWithRawResponse", - "AsyncAgentsResourceWithRawResponse", - "AgentsResourceWithStreamingResponse", - "AsyncAgentsResourceWithStreamingResponse", + "BrowserPoolsResource", + "AsyncBrowserPoolsResource", + "BrowserPoolsResourceWithRawResponse", + "AsyncBrowserPoolsResourceWithRawResponse", + "BrowserPoolsResourceWithStreamingResponse", + "AsyncBrowserPoolsResourceWithStreamingResponse", ] diff --git a/src/kernel/resources/agents/__init__.py b/src/kernel/resources/agents/__init__.py deleted file mode 100644 index cb159eb7..00000000 --- a/src/kernel/resources/agents/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from .auth import ( - AuthResource, - AsyncAuthResource, - AuthResourceWithRawResponse, - AsyncAuthResourceWithRawResponse, - AuthResourceWithStreamingResponse, - AsyncAuthResourceWithStreamingResponse, -) -from .agents import ( - AgentsResource, - AsyncAgentsResource, - AgentsResourceWithRawResponse, - AsyncAgentsResourceWithRawResponse, - AgentsResourceWithStreamingResponse, - AsyncAgentsResourceWithStreamingResponse, -) - -__all__ = [ - "AuthResource", - "AsyncAuthResource", - "AuthResourceWithRawResponse", - "AsyncAuthResourceWithRawResponse", - "AuthResourceWithStreamingResponse", - "AsyncAuthResourceWithStreamingResponse", - "AgentsResource", - "AsyncAgentsResource", - "AgentsResourceWithRawResponse", - "AsyncAgentsResourceWithRawResponse", - "AgentsResourceWithStreamingResponse", - "AsyncAgentsResourceWithStreamingResponse", -] diff --git a/src/kernel/resources/agents/agents.py b/src/kernel/resources/agents/agents.py deleted file mode 100644 index b7bb580c..00000000 --- a/src/kernel/resources/agents/agents.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 ..._compat import cached_property -from .auth.auth import ( - AuthResource, - AsyncAuthResource, - AuthResourceWithRawResponse, - AsyncAuthResourceWithRawResponse, - AuthResourceWithStreamingResponse, - AsyncAuthResourceWithStreamingResponse, -) -from ..._resource import SyncAPIResource, AsyncAPIResource - -__all__ = ["AgentsResource", "AsyncAgentsResource"] - - -class AgentsResource(SyncAPIResource): - @cached_property - def auth(self) -> AuthResource: - return AuthResource(self._client) - - @cached_property - def with_raw_response(self) -> AgentsResourceWithRawResponse: - """ - 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers - """ - return AgentsResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AgentsResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response - """ - return AgentsResourceWithStreamingResponse(self) - - -class AsyncAgentsResource(AsyncAPIResource): - @cached_property - def auth(self) -> AsyncAuthResource: - return AsyncAuthResource(self._client) - - @cached_property - def with_raw_response(self) -> AsyncAgentsResourceWithRawResponse: - """ - 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers - """ - return AsyncAgentsResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncAgentsResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response - """ - return AsyncAgentsResourceWithStreamingResponse(self) - - -class AgentsResourceWithRawResponse: - def __init__(self, agents: AgentsResource) -> None: - self._agents = agents - - @cached_property - def auth(self) -> AuthResourceWithRawResponse: - return AuthResourceWithRawResponse(self._agents.auth) - - -class AsyncAgentsResourceWithRawResponse: - def __init__(self, agents: AsyncAgentsResource) -> None: - self._agents = agents - - @cached_property - def auth(self) -> AsyncAuthResourceWithRawResponse: - return AsyncAuthResourceWithRawResponse(self._agents.auth) - - -class AgentsResourceWithStreamingResponse: - def __init__(self, agents: AgentsResource) -> None: - self._agents = agents - - @cached_property - def auth(self) -> AuthResourceWithStreamingResponse: - return AuthResourceWithStreamingResponse(self._agents.auth) - - -class AsyncAgentsResourceWithStreamingResponse: - def __init__(self, agents: AsyncAgentsResource) -> None: - self._agents = agents - - @cached_property - def auth(self) -> AsyncAuthResourceWithStreamingResponse: - return AsyncAuthResourceWithStreamingResponse(self._agents.auth) diff --git a/src/kernel/resources/agents/auth/__init__.py b/src/kernel/resources/agents/auth/__init__.py deleted file mode 100644 index d9853204..00000000 --- a/src/kernel/resources/agents/auth/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from .auth import ( - AuthResource, - AsyncAuthResource, - AuthResourceWithRawResponse, - AsyncAuthResourceWithRawResponse, - AuthResourceWithStreamingResponse, - AsyncAuthResourceWithStreamingResponse, -) -from .runs import ( - RunsResource, - AsyncRunsResource, - RunsResourceWithRawResponse, - AsyncRunsResourceWithRawResponse, - RunsResourceWithStreamingResponse, - AsyncRunsResourceWithStreamingResponse, -) - -__all__ = [ - "RunsResource", - "AsyncRunsResource", - "RunsResourceWithRawResponse", - "AsyncRunsResourceWithRawResponse", - "RunsResourceWithStreamingResponse", - "AsyncRunsResourceWithStreamingResponse", - "AuthResource", - "AsyncAuthResource", - "AuthResourceWithRawResponse", - "AsyncAuthResourceWithRawResponse", - "AuthResourceWithStreamingResponse", - "AsyncAuthResourceWithStreamingResponse", -] diff --git a/src/kernel/resources/agents/auth/auth.py b/src/kernel/resources/agents/auth/auth.py deleted file mode 100644 index 2e099095..00000000 --- a/src/kernel/resources/agents/auth/auth.py +++ /dev/null @@ -1,239 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import httpx - -from .runs import ( - RunsResource, - AsyncRunsResource, - RunsResourceWithRawResponse, - AsyncRunsResourceWithRawResponse, - RunsResourceWithStreamingResponse, - AsyncRunsResourceWithStreamingResponse, -) -from ...._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from ...._utils import maybe_transform, async_maybe_transform -from ...._compat import cached_property -from ...._resource import SyncAPIResource, AsyncAPIResource -from ...._response import ( - to_raw_response_wrapper, - to_streamed_response_wrapper, - async_to_raw_response_wrapper, - async_to_streamed_response_wrapper, -) -from ...._base_client import make_request_options -from ....types.agents import auth_start_params -from ....types.agents.agent_auth_start_response import AgentAuthStartResponse - -__all__ = ["AuthResource", "AsyncAuthResource"] - - -class AuthResource(SyncAPIResource): - @cached_property - def runs(self) -> RunsResource: - return RunsResource(self._client) - - @cached_property - def with_raw_response(self) -> AuthResourceWithRawResponse: - """ - 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers - """ - return AuthResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AuthResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response - """ - return AuthResourceWithStreamingResponse(self) - - def start( - self, - *, - profile_name: str, - target_domain: str, - app_logo_url: str | Omit = omit, - proxy: auth_start_params.Proxy | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> AgentAuthStartResponse: - """Creates a browser session and returns a handoff code for the hosted flow. - - Uses - standard API key or JWT authentication (not the JWT returned by the exchange - endpoint). - - Args: - profile_name: Name of the profile to use for this flow - - target_domain: Target domain for authentication - - app_logo_url: Optional logo URL for the application - - proxy: Optional proxy configuration - - 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( - "/agents/auth/start", - body=maybe_transform( - { - "profile_name": profile_name, - "target_domain": target_domain, - "app_logo_url": app_logo_url, - "proxy": proxy, - }, - auth_start_params.AuthStartParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AgentAuthStartResponse, - ) - - -class AsyncAuthResource(AsyncAPIResource): - @cached_property - def runs(self) -> AsyncRunsResource: - return AsyncRunsResource(self._client) - - @cached_property - def with_raw_response(self) -> AsyncAuthResourceWithRawResponse: - """ - 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers - """ - return AsyncAuthResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncAuthResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response - """ - return AsyncAuthResourceWithStreamingResponse(self) - - async def start( - self, - *, - profile_name: str, - target_domain: str, - app_logo_url: str | Omit = omit, - proxy: auth_start_params.Proxy | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> AgentAuthStartResponse: - """Creates a browser session and returns a handoff code for the hosted flow. - - Uses - standard API key or JWT authentication (not the JWT returned by the exchange - endpoint). - - Args: - profile_name: Name of the profile to use for this flow - - target_domain: Target domain for authentication - - app_logo_url: Optional logo URL for the application - - proxy: Optional proxy configuration - - 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( - "/agents/auth/start", - body=await async_maybe_transform( - { - "profile_name": profile_name, - "target_domain": target_domain, - "app_logo_url": app_logo_url, - "proxy": proxy, - }, - auth_start_params.AuthStartParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AgentAuthStartResponse, - ) - - -class AuthResourceWithRawResponse: - def __init__(self, auth: AuthResource) -> None: - self._auth = auth - - self.start = to_raw_response_wrapper( - auth.start, - ) - - @cached_property - def runs(self) -> RunsResourceWithRawResponse: - return RunsResourceWithRawResponse(self._auth.runs) - - -class AsyncAuthResourceWithRawResponse: - def __init__(self, auth: AsyncAuthResource) -> None: - self._auth = auth - - self.start = async_to_raw_response_wrapper( - auth.start, - ) - - @cached_property - def runs(self) -> AsyncRunsResourceWithRawResponse: - return AsyncRunsResourceWithRawResponse(self._auth.runs) - - -class AuthResourceWithStreamingResponse: - def __init__(self, auth: AuthResource) -> None: - self._auth = auth - - self.start = to_streamed_response_wrapper( - auth.start, - ) - - @cached_property - def runs(self) -> RunsResourceWithStreamingResponse: - return RunsResourceWithStreamingResponse(self._auth.runs) - - -class AsyncAuthResourceWithStreamingResponse: - def __init__(self, auth: AsyncAuthResource) -> None: - self._auth = auth - - self.start = async_to_streamed_response_wrapper( - auth.start, - ) - - @cached_property - def runs(self) -> AsyncRunsResourceWithStreamingResponse: - return AsyncRunsResourceWithStreamingResponse(self._auth.runs) diff --git a/src/kernel/resources/agents/auth/runs.py b/src/kernel/resources/agents/auth/runs.py deleted file mode 100644 index 6ea09403..00000000 --- a/src/kernel/resources/agents/auth/runs.py +++ /dev/null @@ -1,434 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Dict - -import httpx - -from ...._types import Body, Query, Headers, NotGiven, not_given -from ...._utils import maybe_transform, async_maybe_transform -from ...._compat import cached_property -from ...._resource import SyncAPIResource, AsyncAPIResource -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.agents.auth import run_submit_params, run_exchange_params -from ....types.agents.agent_auth_run_response import AgentAuthRunResponse -from ....types.agents.agent_auth_submit_response import AgentAuthSubmitResponse -from ....types.agents.auth.run_exchange_response import RunExchangeResponse -from ....types.agents.agent_auth_discover_response import AgentAuthDiscoverResponse - -__all__ = ["RunsResource", "AsyncRunsResource"] - - -class RunsResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> RunsResourceWithRawResponse: - """ - 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers - """ - return RunsResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> RunsResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response - """ - return RunsResourceWithStreamingResponse(self) - - def retrieve( - self, - run_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, - ) -> AgentAuthRunResponse: - """Returns run details including app_name and target_domain. - - Uses the JWT returned - by the exchange endpoint, or standard API key or JWT authentication. - - 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 run_id: - raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") - return self._get( - f"/agents/auth/runs/{run_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AgentAuthRunResponse, - ) - - def discover( - self, - run_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, - ) -> AgentAuthDiscoverResponse: - """ - Inspects the target site to detect logged-in state or discover required fields. - Returns 200 with success: true when fields are found, or 4xx/5xx for failures. - Requires the JWT returned by the exchange endpoint. - - 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 run_id: - raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") - return self._post( - f"/agents/auth/runs/{run_id}/discover", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AgentAuthDiscoverResponse, - ) - - def exchange( - self, - run_id: str, - *, - code: 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, - ) -> RunExchangeResponse: - """Validates the handoff code and returns a JWT token for subsequent requests. - - No - authentication required (the handoff code serves as the credential). - - Args: - code: Handoff code from start endpoint - - 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 run_id: - raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") - return self._post( - f"/agents/auth/runs/{run_id}/exchange", - body=maybe_transform({"code": code}, run_exchange_params.RunExchangeParams), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=RunExchangeResponse, - ) - - def submit( - self, - run_id: str, - *, - field_values: Dict[str, 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, - ) -> AgentAuthSubmitResponse: - """ - Submits field values for the discovered login form and may return additional - auth fields or success. Requires the JWT returned by the exchange endpoint. - - Args: - field_values: Values for the discovered login fields - - 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 run_id: - raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") - return self._post( - f"/agents/auth/runs/{run_id}/submit", - body=maybe_transform({"field_values": field_values}, run_submit_params.RunSubmitParams), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AgentAuthSubmitResponse, - ) - - -class AsyncRunsResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncRunsResourceWithRawResponse: - """ - 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers - """ - return AsyncRunsResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncRunsResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response - """ - return AsyncRunsResourceWithStreamingResponse(self) - - async def retrieve( - self, - run_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, - ) -> AgentAuthRunResponse: - """Returns run details including app_name and target_domain. - - Uses the JWT returned - by the exchange endpoint, or standard API key or JWT authentication. - - 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 run_id: - raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") - return await self._get( - f"/agents/auth/runs/{run_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AgentAuthRunResponse, - ) - - async def discover( - self, - run_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, - ) -> AgentAuthDiscoverResponse: - """ - Inspects the target site to detect logged-in state or discover required fields. - Returns 200 with success: true when fields are found, or 4xx/5xx for failures. - Requires the JWT returned by the exchange endpoint. - - 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 run_id: - raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") - return await self._post( - f"/agents/auth/runs/{run_id}/discover", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AgentAuthDiscoverResponse, - ) - - async def exchange( - self, - run_id: str, - *, - code: 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, - ) -> RunExchangeResponse: - """Validates the handoff code and returns a JWT token for subsequent requests. - - No - authentication required (the handoff code serves as the credential). - - Args: - code: Handoff code from start endpoint - - 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 run_id: - raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") - return await self._post( - f"/agents/auth/runs/{run_id}/exchange", - body=await async_maybe_transform({"code": code}, run_exchange_params.RunExchangeParams), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=RunExchangeResponse, - ) - - async def submit( - self, - run_id: str, - *, - field_values: Dict[str, 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, - ) -> AgentAuthSubmitResponse: - """ - Submits field values for the discovered login form and may return additional - auth fields or success. Requires the JWT returned by the exchange endpoint. - - Args: - field_values: Values for the discovered login fields - - 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 run_id: - raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") - return await self._post( - f"/agents/auth/runs/{run_id}/submit", - body=await async_maybe_transform({"field_values": field_values}, run_submit_params.RunSubmitParams), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AgentAuthSubmitResponse, - ) - - -class RunsResourceWithRawResponse: - def __init__(self, runs: RunsResource) -> None: - self._runs = runs - - self.retrieve = to_raw_response_wrapper( - runs.retrieve, - ) - self.discover = to_raw_response_wrapper( - runs.discover, - ) - self.exchange = to_raw_response_wrapper( - runs.exchange, - ) - self.submit = to_raw_response_wrapper( - runs.submit, - ) - - -class AsyncRunsResourceWithRawResponse: - def __init__(self, runs: AsyncRunsResource) -> None: - self._runs = runs - - self.retrieve = async_to_raw_response_wrapper( - runs.retrieve, - ) - self.discover = async_to_raw_response_wrapper( - runs.discover, - ) - self.exchange = async_to_raw_response_wrapper( - runs.exchange, - ) - self.submit = async_to_raw_response_wrapper( - runs.submit, - ) - - -class RunsResourceWithStreamingResponse: - def __init__(self, runs: RunsResource) -> None: - self._runs = runs - - self.retrieve = to_streamed_response_wrapper( - runs.retrieve, - ) - self.discover = to_streamed_response_wrapper( - runs.discover, - ) - self.exchange = to_streamed_response_wrapper( - runs.exchange, - ) - self.submit = to_streamed_response_wrapper( - runs.submit, - ) - - -class AsyncRunsResourceWithStreamingResponse: - def __init__(self, runs: AsyncRunsResource) -> None: - self._runs = runs - - self.retrieve = async_to_streamed_response_wrapper( - runs.retrieve, - ) - self.discover = async_to_streamed_response_wrapper( - runs.discover, - ) - self.exchange = async_to_streamed_response_wrapper( - runs.exchange, - ) - self.submit = async_to_streamed_response_wrapper( - runs.submit, - ) diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py new file mode 100644 index 00000000..d085d515 --- /dev/null +++ b/src/kernel/resources/browser_pools.py @@ -0,0 +1,1022 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Iterable + +import httpx + +from ..types import ( + browser_pool_create_params, + browser_pool_delete_params, + browser_pool_update_params, + browser_pool_acquire_params, + browser_pool_release_params, +) +from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given +from .._utils import maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +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.browser_pool import BrowserPool +from ..types.browser_pool_list_response import BrowserPoolListResponse +from ..types.browser_pool_acquire_response import BrowserPoolAcquireResponse +from ..types.shared_params.browser_profile import BrowserProfile +from ..types.shared_params.browser_viewport import BrowserViewport +from ..types.shared_params.browser_extension import BrowserExtension + +__all__ = ["BrowserPoolsResource", "AsyncBrowserPoolsResource"] + + +class BrowserPoolsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> BrowserPoolsResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return BrowserPoolsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> BrowserPoolsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return BrowserPoolsResourceWithStreamingResponse(self) + + def create( + self, + *, + size: int, + extensions: Iterable[BrowserExtension] | Omit = omit, + fill_rate_per_minute: int | Omit = omit, + headless: bool | Omit = omit, + kiosk_mode: bool | Omit = omit, + name: str | Omit = omit, + profile: BrowserProfile | Omit = omit, + proxy_id: str | Omit = omit, + stealth: bool | Omit = omit, + timeout_seconds: int | Omit = omit, + viewport: BrowserViewport | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> BrowserPool: + """ + Create a new browser pool with the specified configuration and size. + + Args: + size: Number of browsers to create in the pool + + extensions: List of browser extensions to load into the session. Provide each by id or name. + + fill_rate_per_minute: Percentage of the pool to fill per minute. Defaults to 10%. + + headless: If true, launches the browser using a headless image. Defaults to false. + + kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live + view. + + name: Optional name for the browser pool. Must be unique within the organization. + + profile: Profile selection for the browser session. Provide either id or name. If + specified, the matching profile will be loaded into the browser session. + Profiles must be created beforehand. + + proxy_id: Optional proxy to associate to the browser session. Must reference a proxy + belonging to the caller's org. + + stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot + mechanisms. + + timeout_seconds: Default idle timeout in seconds for browsers acquired from this pool before they + are destroyed. Defaults to 600 seconds if not specified + + viewport: Initial browser window size in pixels with optional refresh rate. If omitted, + image defaults apply (commonly 1024x768@60). Only specific viewport + configurations are supported. The server will reject unsupported combinations. + Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will + be automatically determined from the width and height if they match a supported + configuration exactly. Note: Higher resolutions may affect the responsiveness of + live view browser + + 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_pools", + body=maybe_transform( + { + "size": size, + "extensions": extensions, + "fill_rate_per_minute": fill_rate_per_minute, + "headless": headless, + "kiosk_mode": kiosk_mode, + "name": name, + "profile": profile, + "proxy_id": proxy_id, + "stealth": stealth, + "timeout_seconds": timeout_seconds, + "viewport": viewport, + }, + browser_pool_create_params.BrowserPoolCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BrowserPool, + ) + + def retrieve( + self, + id_or_name: 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, + ) -> BrowserPool: + """ + Retrieve details for a single browser pool by its ID or name. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") + return self._get( + f"/browser_pools/{id_or_name}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BrowserPool, + ) + + def update( + self, + id_or_name: str, + *, + size: int, + discard_all_idle: bool | Omit = omit, + extensions: Iterable[BrowserExtension] | Omit = omit, + fill_rate_per_minute: int | Omit = omit, + headless: bool | Omit = omit, + kiosk_mode: bool | Omit = omit, + name: str | Omit = omit, + profile: BrowserProfile | Omit = omit, + proxy_id: str | Omit = omit, + stealth: bool | Omit = omit, + timeout_seconds: int | Omit = omit, + viewport: BrowserViewport | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> BrowserPool: + """ + Updates the configuration used to create browsers in the pool. + + Args: + size: Number of browsers to create in the pool + + discard_all_idle: Whether to discard all idle browsers and rebuild the pool immediately. Defaults + to true. + + extensions: List of browser extensions to load into the session. Provide each by id or name. + + fill_rate_per_minute: Percentage of the pool to fill per minute. Defaults to 10%. + + headless: If true, launches the browser using a headless image. Defaults to false. + + kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live + view. + + name: Optional name for the browser pool. Must be unique within the organization. + + profile: Profile selection for the browser session. Provide either id or name. If + specified, the matching profile will be loaded into the browser session. + Profiles must be created beforehand. + + proxy_id: Optional proxy to associate to the browser session. Must reference a proxy + belonging to the caller's org. + + stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot + mechanisms. + + timeout_seconds: Default idle timeout in seconds for browsers acquired from this pool before they + are destroyed. Defaults to 600 seconds if not specified + + viewport: Initial browser window size in pixels with optional refresh rate. If omitted, + image defaults apply (commonly 1024x768@60). Only specific viewport + configurations are supported. The server will reject unsupported combinations. + Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will + be automatically determined from the width and height if they match a supported + configuration exactly. Note: Higher resolutions may affect the responsiveness of + live view browser + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") + return self._patch( + f"/browser_pools/{id_or_name}", + body=maybe_transform( + { + "size": size, + "discard_all_idle": discard_all_idle, + "extensions": extensions, + "fill_rate_per_minute": fill_rate_per_minute, + "headless": headless, + "kiosk_mode": kiosk_mode, + "name": name, + "profile": profile, + "proxy_id": proxy_id, + "stealth": stealth, + "timeout_seconds": timeout_seconds, + "viewport": viewport, + }, + browser_pool_update_params.BrowserPoolUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BrowserPool, + ) + + def list( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> BrowserPoolListResponse: + """List browser pools owned by the caller's organization.""" + return self._get( + "/browser_pools", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BrowserPoolListResponse, + ) + + def delete( + self, + id_or_name: str, + *, + force: bool | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """Delete a browser pool and all browsers in it. + + By default, deletion is blocked if + browsers are currently leased. Use force=true to terminate leased browsers. + + Args: + force: If true, force delete even if browsers are currently leased. Leased browsers + will be terminated. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + f"/browser_pools/{id_or_name}", + body=maybe_transform({"force": force}, browser_pool_delete_params.BrowserPoolDeleteParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + def acquire( + self, + id_or_name: str, + *, + acquire_timeout_seconds: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> BrowserPoolAcquireResponse: + """Long-polling endpoint to acquire a browser from the pool. + + Returns immediately + when a browser is available, or returns 204 No Content when the poll times out. + The client should retry the request to continue waiting for a browser. The + acquired browser will use the pool's timeout_seconds for its idle timeout. + + Args: + acquire_timeout_seconds: Maximum number of seconds to wait for a browser to be available. Defaults to the + calculated time it would take to fill the pool at the currently configured fill + rate. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") + return self._post( + f"/browser_pools/{id_or_name}/acquire", + body=maybe_transform( + {"acquire_timeout_seconds": acquire_timeout_seconds}, + browser_pool_acquire_params.BrowserPoolAcquireParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BrowserPoolAcquireResponse, + ) + + def flush( + self, + id_or_name: 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: + """ + Destroys all idle browsers in the pool; leased browsers are not affected. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._post( + f"/browser_pools/{id_or_name}/flush", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + def release( + self, + id_or_name: str, + *, + session_id: str, + reuse: bool | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + Release a browser back to the pool, optionally recreating the browser instance. + + Args: + session_id: Browser session ID to release back to the pool + + reuse: Whether to reuse the browser instance or destroy it and create a new one. + Defaults to true. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._post( + f"/browser_pools/{id_or_name}/release", + body=maybe_transform( + { + "session_id": session_id, + "reuse": reuse, + }, + browser_pool_release_params.BrowserPoolReleaseParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class AsyncBrowserPoolsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncBrowserPoolsResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncBrowserPoolsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncBrowserPoolsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return AsyncBrowserPoolsResourceWithStreamingResponse(self) + + async def create( + self, + *, + size: int, + extensions: Iterable[BrowserExtension] | Omit = omit, + fill_rate_per_minute: int | Omit = omit, + headless: bool | Omit = omit, + kiosk_mode: bool | Omit = omit, + name: str | Omit = omit, + profile: BrowserProfile | Omit = omit, + proxy_id: str | Omit = omit, + stealth: bool | Omit = omit, + timeout_seconds: int | Omit = omit, + viewport: BrowserViewport | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> BrowserPool: + """ + Create a new browser pool with the specified configuration and size. + + Args: + size: Number of browsers to create in the pool + + extensions: List of browser extensions to load into the session. Provide each by id or name. + + fill_rate_per_minute: Percentage of the pool to fill per minute. Defaults to 10%. + + headless: If true, launches the browser using a headless image. Defaults to false. + + kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live + view. + + name: Optional name for the browser pool. Must be unique within the organization. + + profile: Profile selection for the browser session. Provide either id or name. If + specified, the matching profile will be loaded into the browser session. + Profiles must be created beforehand. + + proxy_id: Optional proxy to associate to the browser session. Must reference a proxy + belonging to the caller's org. + + stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot + mechanisms. + + timeout_seconds: Default idle timeout in seconds for browsers acquired from this pool before they + are destroyed. Defaults to 600 seconds if not specified + + viewport: Initial browser window size in pixels with optional refresh rate. If omitted, + image defaults apply (commonly 1024x768@60). Only specific viewport + configurations are supported. The server will reject unsupported combinations. + Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will + be automatically determined from the width and height if they match a supported + configuration exactly. Note: Higher resolutions may affect the responsiveness of + live view browser + + 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_pools", + body=await async_maybe_transform( + { + "size": size, + "extensions": extensions, + "fill_rate_per_minute": fill_rate_per_minute, + "headless": headless, + "kiosk_mode": kiosk_mode, + "name": name, + "profile": profile, + "proxy_id": proxy_id, + "stealth": stealth, + "timeout_seconds": timeout_seconds, + "viewport": viewport, + }, + browser_pool_create_params.BrowserPoolCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BrowserPool, + ) + + async def retrieve( + self, + id_or_name: 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, + ) -> BrowserPool: + """ + Retrieve details for a single browser pool by its ID or name. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") + return await self._get( + f"/browser_pools/{id_or_name}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BrowserPool, + ) + + async def update( + self, + id_or_name: str, + *, + size: int, + discard_all_idle: bool | Omit = omit, + extensions: Iterable[BrowserExtension] | Omit = omit, + fill_rate_per_minute: int | Omit = omit, + headless: bool | Omit = omit, + kiosk_mode: bool | Omit = omit, + name: str | Omit = omit, + profile: BrowserProfile | Omit = omit, + proxy_id: str | Omit = omit, + stealth: bool | Omit = omit, + timeout_seconds: int | Omit = omit, + viewport: BrowserViewport | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> BrowserPool: + """ + Updates the configuration used to create browsers in the pool. + + Args: + size: Number of browsers to create in the pool + + discard_all_idle: Whether to discard all idle browsers and rebuild the pool immediately. Defaults + to true. + + extensions: List of browser extensions to load into the session. Provide each by id or name. + + fill_rate_per_minute: Percentage of the pool to fill per minute. Defaults to 10%. + + headless: If true, launches the browser using a headless image. Defaults to false. + + kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live + view. + + name: Optional name for the browser pool. Must be unique within the organization. + + profile: Profile selection for the browser session. Provide either id or name. If + specified, the matching profile will be loaded into the browser session. + Profiles must be created beforehand. + + proxy_id: Optional proxy to associate to the browser session. Must reference a proxy + belonging to the caller's org. + + stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot + mechanisms. + + timeout_seconds: Default idle timeout in seconds for browsers acquired from this pool before they + are destroyed. Defaults to 600 seconds if not specified + + viewport: Initial browser window size in pixels with optional refresh rate. If omitted, + image defaults apply (commonly 1024x768@60). Only specific viewport + configurations are supported. The server will reject unsupported combinations. + Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will + be automatically determined from the width and height if they match a supported + configuration exactly. Note: Higher resolutions may affect the responsiveness of + live view browser + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") + return await self._patch( + f"/browser_pools/{id_or_name}", + body=await async_maybe_transform( + { + "size": size, + "discard_all_idle": discard_all_idle, + "extensions": extensions, + "fill_rate_per_minute": fill_rate_per_minute, + "headless": headless, + "kiosk_mode": kiosk_mode, + "name": name, + "profile": profile, + "proxy_id": proxy_id, + "stealth": stealth, + "timeout_seconds": timeout_seconds, + "viewport": viewport, + }, + browser_pool_update_params.BrowserPoolUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BrowserPool, + ) + + async def list( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> BrowserPoolListResponse: + """List browser pools owned by the caller's organization.""" + return await self._get( + "/browser_pools", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BrowserPoolListResponse, + ) + + async def delete( + self, + id_or_name: str, + *, + force: bool | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """Delete a browser pool and all browsers in it. + + By default, deletion is blocked if + browsers are currently leased. Use force=true to terminate leased browsers. + + Args: + force: If true, force delete even if browsers are currently leased. Leased browsers + will be terminated. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + f"/browser_pools/{id_or_name}", + body=await async_maybe_transform({"force": force}, browser_pool_delete_params.BrowserPoolDeleteParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + async def acquire( + self, + id_or_name: str, + *, + acquire_timeout_seconds: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> BrowserPoolAcquireResponse: + """Long-polling endpoint to acquire a browser from the pool. + + Returns immediately + when a browser is available, or returns 204 No Content when the poll times out. + The client should retry the request to continue waiting for a browser. The + acquired browser will use the pool's timeout_seconds for its idle timeout. + + Args: + acquire_timeout_seconds: Maximum number of seconds to wait for a browser to be available. Defaults to the + calculated time it would take to fill the pool at the currently configured fill + rate. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") + return await self._post( + f"/browser_pools/{id_or_name}/acquire", + body=await async_maybe_transform( + {"acquire_timeout_seconds": acquire_timeout_seconds}, + browser_pool_acquire_params.BrowserPoolAcquireParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BrowserPoolAcquireResponse, + ) + + async def flush( + self, + id_or_name: 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: + """ + Destroys all idle browsers in the pool; leased browsers are not affected. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._post( + f"/browser_pools/{id_or_name}/flush", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + async def release( + self, + id_or_name: str, + *, + session_id: str, + reuse: bool | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + Release a browser back to the pool, optionally recreating the browser instance. + + Args: + session_id: Browser session ID to release back to the pool + + reuse: Whether to reuse the browser instance or destroy it and create a new one. + Defaults to true. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._post( + f"/browser_pools/{id_or_name}/release", + body=await async_maybe_transform( + { + "session_id": session_id, + "reuse": reuse, + }, + browser_pool_release_params.BrowserPoolReleaseParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class BrowserPoolsResourceWithRawResponse: + def __init__(self, browser_pools: BrowserPoolsResource) -> None: + self._browser_pools = browser_pools + + self.create = to_raw_response_wrapper( + browser_pools.create, + ) + self.retrieve = to_raw_response_wrapper( + browser_pools.retrieve, + ) + self.update = to_raw_response_wrapper( + browser_pools.update, + ) + self.list = to_raw_response_wrapper( + browser_pools.list, + ) + self.delete = to_raw_response_wrapper( + browser_pools.delete, + ) + self.acquire = to_raw_response_wrapper( + browser_pools.acquire, + ) + self.flush = to_raw_response_wrapper( + browser_pools.flush, + ) + self.release = to_raw_response_wrapper( + browser_pools.release, + ) + + +class AsyncBrowserPoolsResourceWithRawResponse: + def __init__(self, browser_pools: AsyncBrowserPoolsResource) -> None: + self._browser_pools = browser_pools + + self.create = async_to_raw_response_wrapper( + browser_pools.create, + ) + self.retrieve = async_to_raw_response_wrapper( + browser_pools.retrieve, + ) + self.update = async_to_raw_response_wrapper( + browser_pools.update, + ) + self.list = async_to_raw_response_wrapper( + browser_pools.list, + ) + self.delete = async_to_raw_response_wrapper( + browser_pools.delete, + ) + self.acquire = async_to_raw_response_wrapper( + browser_pools.acquire, + ) + self.flush = async_to_raw_response_wrapper( + browser_pools.flush, + ) + self.release = async_to_raw_response_wrapper( + browser_pools.release, + ) + + +class BrowserPoolsResourceWithStreamingResponse: + def __init__(self, browser_pools: BrowserPoolsResource) -> None: + self._browser_pools = browser_pools + + self.create = to_streamed_response_wrapper( + browser_pools.create, + ) + self.retrieve = to_streamed_response_wrapper( + browser_pools.retrieve, + ) + self.update = to_streamed_response_wrapper( + browser_pools.update, + ) + self.list = to_streamed_response_wrapper( + browser_pools.list, + ) + self.delete = to_streamed_response_wrapper( + browser_pools.delete, + ) + self.acquire = to_streamed_response_wrapper( + browser_pools.acquire, + ) + self.flush = to_streamed_response_wrapper( + browser_pools.flush, + ) + self.release = to_streamed_response_wrapper( + browser_pools.release, + ) + + +class AsyncBrowserPoolsResourceWithStreamingResponse: + def __init__(self, browser_pools: AsyncBrowserPoolsResource) -> None: + self._browser_pools = browser_pools + + self.create = async_to_streamed_response_wrapper( + browser_pools.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + browser_pools.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + browser_pools.update, + ) + self.list = async_to_streamed_response_wrapper( + browser_pools.list, + ) + self.delete = async_to_streamed_response_wrapper( + browser_pools.delete, + ) + self.acquire = async_to_streamed_response_wrapper( + browser_pools.acquire, + ) + self.flush = async_to_streamed_response_wrapper( + browser_pools.flush, + ) + self.release = async_to_streamed_response_wrapper( + browser_pools.release, + ) diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 84a4fe48..f41b46ad 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -76,6 +76,9 @@ from ...types.browser_create_response import BrowserCreateResponse from ...types.browser_persistence_param import BrowserPersistenceParam from ...types.browser_retrieve_response import BrowserRetrieveResponse +from ...types.shared_params.browser_profile import BrowserProfile +from ...types.shared_params.browser_viewport import BrowserViewport +from ...types.shared_params.browser_extension import BrowserExtension __all__ = ["BrowsersResource", "AsyncBrowsersResource"] @@ -127,16 +130,16 @@ def with_streaming_response(self) -> BrowsersResourceWithStreamingResponse: def create( self, *, - extensions: Iterable[browser_create_params.Extension] | Omit = omit, + extensions: Iterable[BrowserExtension] | Omit = omit, headless: bool | Omit = omit, invocation_id: str | Omit = omit, kiosk_mode: bool | Omit = omit, persistence: BrowserPersistenceParam | Omit = omit, - profile: browser_create_params.Profile | Omit = omit, + profile: BrowserProfile | Omit = omit, proxy_id: str | Omit = omit, stealth: bool | Omit = omit, timeout_seconds: int | Omit = omit, - viewport: browser_create_params.Viewport | Omit = omit, + viewport: BrowserViewport | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -470,16 +473,16 @@ def with_streaming_response(self) -> AsyncBrowsersResourceWithStreamingResponse: async def create( self, *, - extensions: Iterable[browser_create_params.Extension] | Omit = omit, + extensions: Iterable[BrowserExtension] | Omit = omit, headless: bool | Omit = omit, invocation_id: str | Omit = omit, kiosk_mode: bool | Omit = omit, persistence: BrowserPersistenceParam | Omit = omit, - profile: browser_create_params.Profile | Omit = omit, + profile: BrowserProfile | Omit = omit, proxy_id: str | Omit = omit, stealth: bool | Omit = omit, timeout_seconds: int | Omit = omit, - viewport: browser_create_params.Viewport | Omit = omit, + viewport: BrowserViewport | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 208a8bda..45b0c4ba 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -8,15 +8,20 @@ ErrorEvent as ErrorEvent, ErrorModel as ErrorModel, ErrorDetail as ErrorDetail, + BrowserProfile as BrowserProfile, HeartbeatEvent as HeartbeatEvent, + BrowserViewport as BrowserViewport, + BrowserExtension as BrowserExtension, ) from .profile import Profile as Profile +from .browser_pool import BrowserPool as BrowserPool from .app_list_params import AppListParams as AppListParams from .app_list_response import AppListResponse as AppListResponse from .browser_list_params import BrowserListParams as BrowserListParams from .browser_persistence import BrowserPersistence as BrowserPersistence from .proxy_create_params import ProxyCreateParams as ProxyCreateParams from .proxy_list_response import ProxyListResponse as ProxyListResponse +from .browser_pool_request import BrowserPoolRequest as BrowserPoolRequest from .browser_create_params import BrowserCreateParams as BrowserCreateParams from .browser_delete_params import BrowserDeleteParams as BrowserDeleteParams from .browser_list_response import BrowserListResponse as BrowserListResponse @@ -41,13 +46,20 @@ from .browser_persistence_param import BrowserPersistenceParam as BrowserPersistenceParam from .browser_retrieve_response import BrowserRetrieveResponse as BrowserRetrieveResponse from .extension_upload_response import ExtensionUploadResponse as ExtensionUploadResponse +from .browser_pool_create_params import BrowserPoolCreateParams as BrowserPoolCreateParams +from .browser_pool_delete_params import BrowserPoolDeleteParams as BrowserPoolDeleteParams +from .browser_pool_list_response import BrowserPoolListResponse as BrowserPoolListResponse +from .browser_pool_update_params import BrowserPoolUpdateParams as BrowserPoolUpdateParams from .deployment_create_response import DeploymentCreateResponse as DeploymentCreateResponse from .deployment_follow_response import DeploymentFollowResponse as DeploymentFollowResponse from .invocation_create_response import InvocationCreateResponse as InvocationCreateResponse from .invocation_follow_response import InvocationFollowResponse as InvocationFollowResponse from .invocation_update_response import InvocationUpdateResponse as InvocationUpdateResponse +from .browser_pool_acquire_params import BrowserPoolAcquireParams as BrowserPoolAcquireParams +from .browser_pool_release_params import BrowserPoolReleaseParams as BrowserPoolReleaseParams from .deployment_retrieve_response import DeploymentRetrieveResponse as DeploymentRetrieveResponse from .invocation_retrieve_response import InvocationRetrieveResponse as InvocationRetrieveResponse +from .browser_pool_acquire_response import BrowserPoolAcquireResponse as BrowserPoolAcquireResponse from .browser_load_extensions_params import BrowserLoadExtensionsParams as BrowserLoadExtensionsParams from .extension_download_from_chrome_store_params import ( ExtensionDownloadFromChromeStoreParams as ExtensionDownloadFromChromeStoreParams, diff --git a/src/kernel/types/agents/__init__.py b/src/kernel/types/agents/__init__.py deleted file mode 100644 index e8c22774..00000000 --- a/src/kernel/types/agents/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from .discovered_field import DiscoveredField as DiscoveredField -from .auth_start_params import AuthStartParams as AuthStartParams -from .agent_auth_run_response import AgentAuthRunResponse as AgentAuthRunResponse -from .agent_auth_start_response import AgentAuthStartResponse as AgentAuthStartResponse -from .agent_auth_submit_response import AgentAuthSubmitResponse as AgentAuthSubmitResponse -from .agent_auth_discover_response import AgentAuthDiscoverResponse as AgentAuthDiscoverResponse diff --git a/src/kernel/types/agents/agent_auth_discover_response.py b/src/kernel/types/agents/agent_auth_discover_response.py deleted file mode 100644 index 000bdec2..00000000 --- a/src/kernel/types/agents/agent_auth_discover_response.py +++ /dev/null @@ -1,28 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Optional - -from ..._models import BaseModel -from .discovered_field import DiscoveredField - -__all__ = ["AgentAuthDiscoverResponse"] - - -class AgentAuthDiscoverResponse(BaseModel): - success: bool - """Whether discovery succeeded""" - - error_message: Optional[str] = None - """Error message if discovery failed""" - - fields: Optional[List[DiscoveredField]] = None - """Discovered form fields (present when success is true)""" - - logged_in: Optional[bool] = None - """Whether user is already logged in""" - - login_url: Optional[str] = None - """URL of the discovered login page""" - - page_title: Optional[str] = None - """Title of the login page""" diff --git a/src/kernel/types/agents/agent_auth_run_response.py b/src/kernel/types/agents/agent_auth_run_response.py deleted file mode 100644 index 0ec0b0bb..00000000 --- a/src/kernel/types/agents/agent_auth_run_response.py +++ /dev/null @@ -1,22 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from datetime import datetime -from typing_extensions import Literal - -from ..._models import BaseModel - -__all__ = ["AgentAuthRunResponse"] - - -class AgentAuthRunResponse(BaseModel): - app_name: str - """App name (org name at time of run creation)""" - - expires_at: datetime - """When the handoff code expires""" - - status: Literal["ACTIVE", "ENDED", "EXPIRED", "CANCELED"] - """Run status""" - - target_domain: str - """Target domain for authentication""" diff --git a/src/kernel/types/agents/agent_auth_start_response.py b/src/kernel/types/agents/agent_auth_start_response.py deleted file mode 100644 index 2855fc2d..00000000 --- a/src/kernel/types/agents/agent_auth_start_response.py +++ /dev/null @@ -1,21 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from datetime import datetime - -from ..._models import BaseModel - -__all__ = ["AgentAuthStartResponse"] - - -class AgentAuthStartResponse(BaseModel): - expires_at: datetime - """When the handoff code expires""" - - handoff_code: str - """One-time code for handoff""" - - hosted_url: str - """URL to redirect user to""" - - run_id: str - """Unique identifier for the run""" diff --git a/src/kernel/types/agents/agent_auth_submit_response.py b/src/kernel/types/agents/agent_auth_submit_response.py deleted file mode 100644 index c57002fb..00000000 --- a/src/kernel/types/agents/agent_auth_submit_response.py +++ /dev/null @@ -1,34 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Optional - -from ..._models import BaseModel -from .discovered_field import DiscoveredField - -__all__ = ["AgentAuthSubmitResponse"] - - -class AgentAuthSubmitResponse(BaseModel): - success: bool - """Whether submission succeeded""" - - additional_fields: Optional[List[DiscoveredField]] = None - """ - Additional fields needed (e.g., OTP) - present when needs_additional_auth is - true - """ - - app_name: Optional[str] = None - """App name (only present when logged_in is true)""" - - error_message: Optional[str] = None - """Error message if submission failed""" - - logged_in: Optional[bool] = None - """Whether user is now logged in""" - - needs_additional_auth: Optional[bool] = None - """Whether additional authentication fields are needed""" - - target_domain: Optional[str] = None - """Target domain (only present when logged_in is true)""" diff --git a/src/kernel/types/agents/auth/__init__.py b/src/kernel/types/agents/auth/__init__.py deleted file mode 100644 index 78a13a38..00000000 --- a/src/kernel/types/agents/auth/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from .run_submit_params import RunSubmitParams as RunSubmitParams -from .run_exchange_params import RunExchangeParams as RunExchangeParams -from .run_exchange_response import RunExchangeResponse as RunExchangeResponse diff --git a/src/kernel/types/agents/auth/run_exchange_params.py b/src/kernel/types/agents/auth/run_exchange_params.py deleted file mode 100644 index 1a23b25d..00000000 --- a/src/kernel/types/agents/auth/run_exchange_params.py +++ /dev/null @@ -1,12 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Required, TypedDict - -__all__ = ["RunExchangeParams"] - - -class RunExchangeParams(TypedDict, total=False): - code: Required[str] - """Handoff code from start endpoint""" diff --git a/src/kernel/types/agents/auth/run_exchange_response.py b/src/kernel/types/agents/auth/run_exchange_response.py deleted file mode 100644 index 347c57c3..00000000 --- a/src/kernel/types/agents/auth/run_exchange_response.py +++ /dev/null @@ -1,13 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from ...._models import BaseModel - -__all__ = ["RunExchangeResponse"] - - -class RunExchangeResponse(BaseModel): - jwt: str - """JWT token with run_id claim (30 minute TTL)""" - - run_id: str - """Run ID""" diff --git a/src/kernel/types/agents/auth/run_submit_params.py b/src/kernel/types/agents/auth/run_submit_params.py deleted file mode 100644 index efaf9ea5..00000000 --- a/src/kernel/types/agents/auth/run_submit_params.py +++ /dev/null @@ -1,13 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Dict -from typing_extensions import Required, TypedDict - -__all__ = ["RunSubmitParams"] - - -class RunSubmitParams(TypedDict, total=False): - field_values: Required[Dict[str, str]] - """Values for the discovered login fields""" diff --git a/src/kernel/types/agents/auth_start_params.py b/src/kernel/types/agents/auth_start_params.py deleted file mode 100644 index 6e0f0c82..00000000 --- a/src/kernel/types/agents/auth_start_params.py +++ /dev/null @@ -1,26 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Required, TypedDict - -__all__ = ["AuthStartParams", "Proxy"] - - -class AuthStartParams(TypedDict, total=False): - profile_name: Required[str] - """Name of the profile to use for this flow""" - - target_domain: Required[str] - """Target domain for authentication""" - - app_logo_url: str - """Optional logo URL for the application""" - - proxy: Proxy - """Optional proxy configuration""" - - -class Proxy(TypedDict, total=False): - proxy_id: str - """ID of the proxy to use""" diff --git a/src/kernel/types/agents/discovered_field.py b/src/kernel/types/agents/discovered_field.py deleted file mode 100644 index 90a4864c..00000000 --- a/src/kernel/types/agents/discovered_field.py +++ /dev/null @@ -1,28 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional -from typing_extensions import Literal - -from ..._models import BaseModel - -__all__ = ["DiscoveredField"] - - -class DiscoveredField(BaseModel): - label: str - """Field label""" - - name: str - """Field name""" - - selector: str - """CSS selector for the field""" - - type: Literal["text", "email", "password", "tel", "number", "url", "code", "checkbox"] - """Field type""" - - placeholder: Optional[str] = None - """Field placeholder""" - - required: Optional[bool] = None - """Whether field is required""" diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index 1e54ce75..d1ff7907 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -3,15 +3,18 @@ from __future__ import annotations from typing import Iterable -from typing_extensions import Required, TypedDict +from typing_extensions import TypedDict from .browser_persistence_param import BrowserPersistenceParam +from .shared_params.browser_profile import BrowserProfile +from .shared_params.browser_viewport import BrowserViewport +from .shared_params.browser_extension import BrowserExtension -__all__ = ["BrowserCreateParams", "Extension", "Profile", "Viewport"] +__all__ = ["BrowserCreateParams"] class BrowserCreateParams(TypedDict, total=False): - extensions: Iterable[Extension] + extensions: Iterable[BrowserExtension] """List of browser extensions to load into the session. Provide each by id or name. @@ -35,7 +38,7 @@ class BrowserCreateParams(TypedDict, total=False): persistence: BrowserPersistenceParam """Optional persistence configuration for the browser session.""" - profile: Profile + profile: BrowserProfile """Profile selection for the browser session. Provide either id or name. If specified, the matching profile will be loaded @@ -64,7 +67,7 @@ class BrowserCreateParams(TypedDict, total=False): specified value. """ - viewport: Viewport + viewport: BrowserViewport """Initial browser window size in pixels with optional refresh rate. If omitted, image defaults apply (commonly 1024x768@60). Only specific viewport @@ -75,45 +78,3 @@ class BrowserCreateParams(TypedDict, total=False): configuration exactly. Note: Higher resolutions may affect the responsiveness of live view browser """ - - -class Extension(TypedDict, total=False): - id: str - """Extension ID to load for this browser session""" - - name: str - """Extension name to load for this browser session (instead of id). - - Must be 1-255 characters, using letters, numbers, dots, underscores, or hyphens. - """ - - -class Profile(TypedDict, total=False): - id: str - """Profile ID to load for this browser session""" - - name: str - """Profile name to load for this browser session (instead of id). - - Must be 1-255 characters, using letters, numbers, dots, underscores, or hyphens. - """ - - save_changes: bool - """ - If true, save changes made during the session back to the profile when the - session ends. - """ - - -class Viewport(TypedDict, total=False): - height: Required[int] - """Browser window height in pixels.""" - - width: Required[int] - """Browser window width in pixels.""" - - refresh_rate: int - """Display refresh rate in Hz. - - If omitted, automatically determined from width and height. - """ diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index 21041eaa..0a5f33d4 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -6,22 +6,9 @@ from .profile import Profile from .._models import BaseModel from .browser_persistence import BrowserPersistence +from .shared.browser_viewport import BrowserViewport -__all__ = ["BrowserCreateResponse", "Viewport"] - - -class Viewport(BaseModel): - height: int - """Browser window height in pixels.""" - - width: int - """Browser window width in pixels.""" - - refresh_rate: Optional[int] = None - """Display refresh rate in Hz. - - If omitted, automatically determined from width and height. - """ +__all__ = ["BrowserCreateResponse"] class BrowserCreateResponse(BaseModel): @@ -64,7 +51,7 @@ class BrowserCreateResponse(BaseModel): proxy_id: Optional[str] = None """ID of the proxy associated with this browser session, if any.""" - viewport: Optional[Viewport] = None + viewport: Optional[BrowserViewport] = None """Initial browser window size in pixels with optional refresh rate. If omitted, image defaults apply (commonly 1024x768@60). Only specific viewport diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index 74978690..d6397291 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -6,22 +6,9 @@ from .profile import Profile from .._models import BaseModel from .browser_persistence import BrowserPersistence +from .shared.browser_viewport import BrowserViewport -__all__ = ["BrowserListResponse", "Viewport"] - - -class Viewport(BaseModel): - height: int - """Browser window height in pixels.""" - - width: int - """Browser window width in pixels.""" - - refresh_rate: Optional[int] = None - """Display refresh rate in Hz. - - If omitted, automatically determined from width and height. - """ +__all__ = ["BrowserListResponse"] class BrowserListResponse(BaseModel): @@ -64,7 +51,7 @@ class BrowserListResponse(BaseModel): proxy_id: Optional[str] = None """ID of the proxy associated with this browser session, if any.""" - viewport: Optional[Viewport] = None + viewport: Optional[BrowserViewport] = None """Initial browser window size in pixels with optional refresh rate. If omitted, image defaults apply (commonly 1024x768@60). Only specific viewport diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py new file mode 100644 index 00000000..5fd30dca --- /dev/null +++ b/src/kernel/types/browser_pool.py @@ -0,0 +1,29 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime + +from .._models import BaseModel +from .browser_pool_request import BrowserPoolRequest + +__all__ = ["BrowserPool"] + + +class BrowserPool(BaseModel): + id: str + """Unique identifier for the browser pool""" + + acquired_count: int + """Number of browsers currently acquired from the pool""" + + available_count: int + """Number of browsers currently available in the pool""" + + browser_pool_config: BrowserPoolRequest + """Configuration used to create all browsers in this pool""" + + created_at: datetime + """Timestamp when the browser pool was created""" + + name: Optional[str] = None + """Browser pool name, if set""" diff --git a/src/kernel/types/browser_pool_acquire_params.py b/src/kernel/types/browser_pool_acquire_params.py new file mode 100644 index 00000000..d0df921a --- /dev/null +++ b/src/kernel/types/browser_pool_acquire_params.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["BrowserPoolAcquireParams"] + + +class BrowserPoolAcquireParams(TypedDict, total=False): + acquire_timeout_seconds: int + """Maximum number of seconds to wait for a browser to be available. + + Defaults to the calculated time it would take to fill the pool at the currently + configured fill rate. + """ diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py new file mode 100644 index 00000000..76ad037b --- /dev/null +++ b/src/kernel/types/browser_pool_acquire_response.py @@ -0,0 +1,64 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime + +from .profile import Profile +from .._models import BaseModel +from .browser_persistence import BrowserPersistence +from .shared.browser_viewport import BrowserViewport + +__all__ = ["BrowserPoolAcquireResponse"] + + +class BrowserPoolAcquireResponse(BaseModel): + cdp_ws_url: str + """Websocket URL for Chrome DevTools Protocol connections to the browser session""" + + created_at: datetime + """When the browser session was created.""" + + headless: bool + """Whether the browser session is running in headless mode.""" + + session_id: str + """Unique identifier for the browser session""" + + stealth: bool + """Whether the browser session is running in stealth mode.""" + + timeout_seconds: int + """The number of seconds of inactivity before the browser session is terminated.""" + + browser_live_view_url: Optional[str] = None + """Remote URL for live viewing the browser session. + + Only available for non-headless browsers. + """ + + deleted_at: Optional[datetime] = None + """When the browser session was soft-deleted. Only present for deleted sessions.""" + + kiosk_mode: Optional[bool] = None + """Whether the browser session is running in kiosk mode.""" + + persistence: Optional[BrowserPersistence] = None + """Optional persistence configuration for the browser session.""" + + profile: Optional[Profile] = None + """Browser profile metadata.""" + + proxy_id: Optional[str] = None + """ID of the proxy associated with this browser session, if any.""" + + viewport: Optional[BrowserViewport] = None + """Initial browser window size in pixels with optional refresh rate. + + If omitted, image defaults apply (commonly 1024x768@60). Only specific viewport + configurations are supported. The server will reject unsupported combinations. + Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will + be automatically determined from the width and height if they match a supported + configuration exactly. Note: Higher resolutions may affect the responsiveness of + live view browser + """ diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py new file mode 100644 index 00000000..c7f87c64 --- /dev/null +++ b/src/kernel/types/browser_pool_create_params.py @@ -0,0 +1,75 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Iterable +from typing_extensions import Required, TypedDict + +from .shared_params.browser_profile import BrowserProfile +from .shared_params.browser_viewport import BrowserViewport +from .shared_params.browser_extension import BrowserExtension + +__all__ = ["BrowserPoolCreateParams"] + + +class BrowserPoolCreateParams(TypedDict, total=False): + size: Required[int] + """Number of browsers to create in the pool""" + + extensions: Iterable[BrowserExtension] + """List of browser extensions to load into the session. + + Provide each by id or name. + """ + + fill_rate_per_minute: int + """Percentage of the pool to fill per minute. Defaults to 10%.""" + + headless: bool + """If true, launches the browser using a headless image. Defaults to false.""" + + kiosk_mode: bool + """ + If true, launches the browser in kiosk mode to hide address bar and tabs in live + view. + """ + + name: str + """Optional name for the browser pool. Must be unique within the organization.""" + + profile: BrowserProfile + """Profile selection for the browser session. + + Provide either id or name. If specified, the matching profile will be loaded + into the browser session. Profiles must be created beforehand. + """ + + proxy_id: str + """Optional proxy to associate to the browser session. + + Must reference a proxy belonging to the caller's org. + """ + + stealth: bool + """ + If true, launches the browser in stealth mode to reduce detection by anti-bot + mechanisms. + """ + + timeout_seconds: int + """ + Default idle timeout in seconds for browsers acquired from this pool before they + are destroyed. Defaults to 600 seconds if not specified + """ + + viewport: BrowserViewport + """Initial browser window size in pixels with optional refresh rate. + + If omitted, image defaults apply (commonly 1024x768@60). Only specific viewport + configurations are supported. The server will reject unsupported combinations. + Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will + be automatically determined from the width and height if they match a supported + configuration exactly. Note: Higher resolutions may affect the responsiveness of + live view browser + """ diff --git a/src/kernel/types/browser_pool_delete_params.py b/src/kernel/types/browser_pool_delete_params.py new file mode 100644 index 00000000..0a63c0f1 --- /dev/null +++ b/src/kernel/types/browser_pool_delete_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["BrowserPoolDeleteParams"] + + +class BrowserPoolDeleteParams(TypedDict, total=False): + force: bool + """If true, force delete even if browsers are currently leased. + + Leased browsers will be terminated. + """ diff --git a/src/kernel/types/browser_pool_list_response.py b/src/kernel/types/browser_pool_list_response.py new file mode 100644 index 00000000..a11c4de2 --- /dev/null +++ b/src/kernel/types/browser_pool_list_response.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List +from typing_extensions import TypeAlias + +from .browser_pool import BrowserPool + +__all__ = ["BrowserPoolListResponse"] + +BrowserPoolListResponse: TypeAlias = List[BrowserPool] diff --git a/src/kernel/types/browser_pool_release_params.py b/src/kernel/types/browser_pool_release_params.py new file mode 100644 index 00000000..104b0b0c --- /dev/null +++ b/src/kernel/types/browser_pool_release_params.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["BrowserPoolReleaseParams"] + + +class BrowserPoolReleaseParams(TypedDict, total=False): + session_id: Required[str] + """Browser session ID to release back to the pool""" + + reuse: bool + """Whether to reuse the browser instance or destroy it and create a new one. + + Defaults to true. + """ diff --git a/src/kernel/types/browser_pool_request.py b/src/kernel/types/browser_pool_request.py new file mode 100644 index 00000000..c25b3a55 --- /dev/null +++ b/src/kernel/types/browser_pool_request.py @@ -0,0 +1,73 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel +from .shared.browser_profile import BrowserProfile +from .shared.browser_viewport import BrowserViewport +from .shared.browser_extension import BrowserExtension + +__all__ = ["BrowserPoolRequest"] + + +class BrowserPoolRequest(BaseModel): + size: int + """Number of browsers to create in the pool""" + + extensions: Optional[List[BrowserExtension]] = None + """List of browser extensions to load into the session. + + Provide each by id or name. + """ + + fill_rate_per_minute: Optional[int] = None + """Percentage of the pool to fill per minute. Defaults to 10%.""" + + headless: Optional[bool] = None + """If true, launches the browser using a headless image. Defaults to false.""" + + kiosk_mode: Optional[bool] = None + """ + If true, launches the browser in kiosk mode to hide address bar and tabs in live + view. + """ + + name: Optional[str] = None + """Optional name for the browser pool. Must be unique within the organization.""" + + profile: Optional[BrowserProfile] = None + """Profile selection for the browser session. + + Provide either id or name. If specified, the matching profile will be loaded + into the browser session. Profiles must be created beforehand. + """ + + proxy_id: Optional[str] = None + """Optional proxy to associate to the browser session. + + Must reference a proxy belonging to the caller's org. + """ + + stealth: Optional[bool] = None + """ + If true, launches the browser in stealth mode to reduce detection by anti-bot + mechanisms. + """ + + timeout_seconds: Optional[int] = None + """ + Default idle timeout in seconds for browsers acquired from this pool before they + are destroyed. Defaults to 600 seconds if not specified + """ + + viewport: Optional[BrowserViewport] = None + """Initial browser window size in pixels with optional refresh rate. + + If omitted, image defaults apply (commonly 1024x768@60). Only specific viewport + configurations are supported. The server will reject unsupported combinations. + Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will + be automatically determined from the width and height if they match a supported + configuration exactly. Note: Higher resolutions may affect the responsiveness of + live view browser + """ diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py new file mode 100644 index 00000000..ed9a7e84 --- /dev/null +++ b/src/kernel/types/browser_pool_update_params.py @@ -0,0 +1,81 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Iterable +from typing_extensions import Required, TypedDict + +from .shared_params.browser_profile import BrowserProfile +from .shared_params.browser_viewport import BrowserViewport +from .shared_params.browser_extension import BrowserExtension + +__all__ = ["BrowserPoolUpdateParams"] + + +class BrowserPoolUpdateParams(TypedDict, total=False): + size: Required[int] + """Number of browsers to create in the pool""" + + discard_all_idle: bool + """Whether to discard all idle browsers and rebuild the pool immediately. + + Defaults to true. + """ + + extensions: Iterable[BrowserExtension] + """List of browser extensions to load into the session. + + Provide each by id or name. + """ + + fill_rate_per_minute: int + """Percentage of the pool to fill per minute. Defaults to 10%.""" + + headless: bool + """If true, launches the browser using a headless image. Defaults to false.""" + + kiosk_mode: bool + """ + If true, launches the browser in kiosk mode to hide address bar and tabs in live + view. + """ + + name: str + """Optional name for the browser pool. Must be unique within the organization.""" + + profile: BrowserProfile + """Profile selection for the browser session. + + Provide either id or name. If specified, the matching profile will be loaded + into the browser session. Profiles must be created beforehand. + """ + + proxy_id: str + """Optional proxy to associate to the browser session. + + Must reference a proxy belonging to the caller's org. + """ + + stealth: bool + """ + If true, launches the browser in stealth mode to reduce detection by anti-bot + mechanisms. + """ + + timeout_seconds: int + """ + Default idle timeout in seconds for browsers acquired from this pool before they + are destroyed. Defaults to 600 seconds if not specified + """ + + viewport: BrowserViewport + """Initial browser window size in pixels with optional refresh rate. + + If omitted, image defaults apply (commonly 1024x768@60). Only specific viewport + configurations are supported. The server will reject unsupported combinations. + Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will + be automatically determined from the width and height if they match a supported + configuration exactly. Note: Higher resolutions may affect the responsiveness of + live view browser + """ diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 527386da..111149b3 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -6,22 +6,9 @@ from .profile import Profile from .._models import BaseModel from .browser_persistence import BrowserPersistence +from .shared.browser_viewport import BrowserViewport -__all__ = ["BrowserRetrieveResponse", "Viewport"] - - -class Viewport(BaseModel): - height: int - """Browser window height in pixels.""" - - width: int - """Browser window width in pixels.""" - - refresh_rate: Optional[int] = None - """Display refresh rate in Hz. - - If omitted, automatically determined from width and height. - """ +__all__ = ["BrowserRetrieveResponse"] class BrowserRetrieveResponse(BaseModel): @@ -64,7 +51,7 @@ class BrowserRetrieveResponse(BaseModel): proxy_id: Optional[str] = None """ID of the proxy associated with this browser session, if any.""" - viewport: Optional[Viewport] = None + viewport: Optional[BrowserViewport] = None """Initial browser window size in pixels with optional refresh rate. If omitted, image defaults apply (commonly 1024x768@60). Only specific viewport diff --git a/src/kernel/types/shared/__init__.py b/src/kernel/types/shared/__init__.py index ea360f12..6b649199 100644 --- a/src/kernel/types/shared/__init__.py +++ b/src/kernel/types/shared/__init__.py @@ -5,4 +5,7 @@ from .error_event import ErrorEvent as ErrorEvent from .error_model import ErrorModel as ErrorModel from .error_detail import ErrorDetail as ErrorDetail +from .browser_profile import BrowserProfile as BrowserProfile from .heartbeat_event import HeartbeatEvent as HeartbeatEvent +from .browser_viewport import BrowserViewport as BrowserViewport +from .browser_extension import BrowserExtension as BrowserExtension diff --git a/src/kernel/types/shared/browser_extension.py b/src/kernel/types/shared/browser_extension.py new file mode 100644 index 00000000..7bc1a5ff --- /dev/null +++ b/src/kernel/types/shared/browser_extension.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = ["BrowserExtension"] + + +class BrowserExtension(BaseModel): + id: Optional[str] = None + """Extension ID to load for this browser session""" + + name: Optional[str] = None + """Extension name to load for this browser session (instead of id). + + Must be 1-255 characters, using letters, numbers, dots, underscores, or hyphens. + """ diff --git a/src/kernel/types/shared/browser_profile.py b/src/kernel/types/shared/browser_profile.py new file mode 100644 index 00000000..5f790ccb --- /dev/null +++ b/src/kernel/types/shared/browser_profile.py @@ -0,0 +1,24 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = ["BrowserProfile"] + + +class BrowserProfile(BaseModel): + id: Optional[str] = None + """Profile ID to load for this browser session""" + + name: Optional[str] = None + """Profile name to load for this browser session (instead of id). + + Must be 1-255 characters, using letters, numbers, dots, underscores, or hyphens. + """ + + save_changes: Optional[bool] = None + """ + If true, save changes made during the session back to the profile when the + session ends. + """ diff --git a/src/kernel/types/shared/browser_viewport.py b/src/kernel/types/shared/browser_viewport.py new file mode 100644 index 00000000..abffcc29 --- /dev/null +++ b/src/kernel/types/shared/browser_viewport.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = ["BrowserViewport"] + + +class BrowserViewport(BaseModel): + height: int + """Browser window height in pixels.""" + + width: int + """Browser window width in pixels.""" + + refresh_rate: Optional[int] = None + """Display refresh rate in Hz. + + If omitted, automatically determined from width and height. + """ diff --git a/src/kernel/types/shared_params/__init__.py b/src/kernel/types/shared_params/__init__.py new file mode 100644 index 00000000..de63c649 --- /dev/null +++ b/src/kernel/types/shared_params/__init__.py @@ -0,0 +1,5 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .browser_profile import BrowserProfile as BrowserProfile +from .browser_viewport import BrowserViewport as BrowserViewport +from .browser_extension import BrowserExtension as BrowserExtension diff --git a/src/kernel/types/shared_params/browser_extension.py b/src/kernel/types/shared_params/browser_extension.py new file mode 100644 index 00000000..d81ac708 --- /dev/null +++ b/src/kernel/types/shared_params/browser_extension.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["BrowserExtension"] + + +class BrowserExtension(TypedDict, total=False): + id: str + """Extension ID to load for this browser session""" + + name: str + """Extension name to load for this browser session (instead of id). + + Must be 1-255 characters, using letters, numbers, dots, underscores, or hyphens. + """ diff --git a/src/kernel/types/shared_params/browser_profile.py b/src/kernel/types/shared_params/browser_profile.py new file mode 100644 index 00000000..e1027d22 --- /dev/null +++ b/src/kernel/types/shared_params/browser_profile.py @@ -0,0 +1,24 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["BrowserProfile"] + + +class BrowserProfile(TypedDict, total=False): + id: str + """Profile ID to load for this browser session""" + + name: str + """Profile name to load for this browser session (instead of id). + + Must be 1-255 characters, using letters, numbers, dots, underscores, or hyphens. + """ + + save_changes: bool + """ + If true, save changes made during the session back to the profile when the + session ends. + """ diff --git a/src/kernel/types/shared_params/browser_viewport.py b/src/kernel/types/shared_params/browser_viewport.py new file mode 100644 index 00000000..b7cb2f0f --- /dev/null +++ b/src/kernel/types/shared_params/browser_viewport.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["BrowserViewport"] + + +class BrowserViewport(TypedDict, total=False): + height: Required[int] + """Browser window height in pixels.""" + + width: Required[int] + """Browser window width in pixels.""" + + refresh_rate: int + """Display refresh rate in Hz. + + If omitted, automatically determined from width and height. + """ diff --git a/tests/api_resources/agents/__init__.py b/tests/api_resources/agents/__init__.py deleted file mode 100644 index fd8019a9..00000000 --- a/tests/api_resources/agents/__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/agents/auth/__init__.py b/tests/api_resources/agents/auth/__init__.py deleted file mode 100644 index fd8019a9..00000000 --- a/tests/api_resources/agents/auth/__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/agents/auth/test_runs.py b/tests/api_resources/agents/auth/test_runs.py deleted file mode 100644 index 25fbdb14..00000000 --- a/tests/api_resources/agents/auth/test_runs.py +++ /dev/null @@ -1,401 +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 kernel import Kernel, AsyncKernel -from tests.utils import assert_matches_type -from kernel.types.agents import AgentAuthRunResponse, AgentAuthSubmitResponse, AgentAuthDiscoverResponse -from kernel.types.agents.auth import RunExchangeResponse - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestRuns: - 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: Kernel) -> None: - run = client.agents.auth.runs.retrieve( - "run_id", - ) - assert_matches_type(AgentAuthRunResponse, run, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_retrieve(self, client: Kernel) -> None: - response = client.agents.auth.runs.with_raw_response.retrieve( - "run_id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - run = response.parse() - assert_matches_type(AgentAuthRunResponse, run, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_retrieve(self, client: Kernel) -> None: - with client.agents.auth.runs.with_streaming_response.retrieve( - "run_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - run = response.parse() - assert_matches_type(AgentAuthRunResponse, run, 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: Kernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `run_id` but received ''"): - client.agents.auth.runs.with_raw_response.retrieve( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_discover(self, client: Kernel) -> None: - run = client.agents.auth.runs.discover( - "run_id", - ) - assert_matches_type(AgentAuthDiscoverResponse, run, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_discover(self, client: Kernel) -> None: - response = client.agents.auth.runs.with_raw_response.discover( - "run_id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - run = response.parse() - assert_matches_type(AgentAuthDiscoverResponse, run, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_discover(self, client: Kernel) -> None: - with client.agents.auth.runs.with_streaming_response.discover( - "run_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - run = response.parse() - assert_matches_type(AgentAuthDiscoverResponse, run, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_discover(self, client: Kernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `run_id` but received ''"): - client.agents.auth.runs.with_raw_response.discover( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_exchange(self, client: Kernel) -> None: - run = client.agents.auth.runs.exchange( - run_id="run_id", - code="otp_abc123xyz", - ) - assert_matches_type(RunExchangeResponse, run, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_exchange(self, client: Kernel) -> None: - response = client.agents.auth.runs.with_raw_response.exchange( - run_id="run_id", - code="otp_abc123xyz", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - run = response.parse() - assert_matches_type(RunExchangeResponse, run, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_exchange(self, client: Kernel) -> None: - with client.agents.auth.runs.with_streaming_response.exchange( - run_id="run_id", - code="otp_abc123xyz", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - run = response.parse() - assert_matches_type(RunExchangeResponse, run, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_exchange(self, client: Kernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `run_id` but received ''"): - client.agents.auth.runs.with_raw_response.exchange( - run_id="", - code="otp_abc123xyz", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_submit(self, client: Kernel) -> None: - run = client.agents.auth.runs.submit( - run_id="run_id", - field_values={ - "email": "user@example.com", - "password": "********", - }, - ) - assert_matches_type(AgentAuthSubmitResponse, run, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_submit(self, client: Kernel) -> None: - response = client.agents.auth.runs.with_raw_response.submit( - run_id="run_id", - field_values={ - "email": "user@example.com", - "password": "********", - }, - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - run = response.parse() - assert_matches_type(AgentAuthSubmitResponse, run, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_submit(self, client: Kernel) -> None: - with client.agents.auth.runs.with_streaming_response.submit( - run_id="run_id", - field_values={ - "email": "user@example.com", - "password": "********", - }, - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - run = response.parse() - assert_matches_type(AgentAuthSubmitResponse, run, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_submit(self, client: Kernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `run_id` but received ''"): - client.agents.auth.runs.with_raw_response.submit( - run_id="", - field_values={ - "email": "user@example.com", - "password": "********", - }, - ) - - -class TestAsyncRuns: - 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: AsyncKernel) -> None: - run = await async_client.agents.auth.runs.retrieve( - "run_id", - ) - assert_matches_type(AgentAuthRunResponse, run, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: - response = await async_client.agents.auth.runs.with_raw_response.retrieve( - "run_id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - run = await response.parse() - assert_matches_type(AgentAuthRunResponse, run, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: - async with async_client.agents.auth.runs.with_streaming_response.retrieve( - "run_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - run = await response.parse() - assert_matches_type(AgentAuthRunResponse, run, 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: AsyncKernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `run_id` but received ''"): - await async_client.agents.auth.runs.with_raw_response.retrieve( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_discover(self, async_client: AsyncKernel) -> None: - run = await async_client.agents.auth.runs.discover( - "run_id", - ) - assert_matches_type(AgentAuthDiscoverResponse, run, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_discover(self, async_client: AsyncKernel) -> None: - response = await async_client.agents.auth.runs.with_raw_response.discover( - "run_id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - run = await response.parse() - assert_matches_type(AgentAuthDiscoverResponse, run, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_discover(self, async_client: AsyncKernel) -> None: - async with async_client.agents.auth.runs.with_streaming_response.discover( - "run_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - run = await response.parse() - assert_matches_type(AgentAuthDiscoverResponse, run, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_discover(self, async_client: AsyncKernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `run_id` but received ''"): - await async_client.agents.auth.runs.with_raw_response.discover( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_exchange(self, async_client: AsyncKernel) -> None: - run = await async_client.agents.auth.runs.exchange( - run_id="run_id", - code="otp_abc123xyz", - ) - assert_matches_type(RunExchangeResponse, run, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_exchange(self, async_client: AsyncKernel) -> None: - response = await async_client.agents.auth.runs.with_raw_response.exchange( - run_id="run_id", - code="otp_abc123xyz", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - run = await response.parse() - assert_matches_type(RunExchangeResponse, run, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_exchange(self, async_client: AsyncKernel) -> None: - async with async_client.agents.auth.runs.with_streaming_response.exchange( - run_id="run_id", - code="otp_abc123xyz", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - run = await response.parse() - assert_matches_type(RunExchangeResponse, run, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_exchange(self, async_client: AsyncKernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `run_id` but received ''"): - await async_client.agents.auth.runs.with_raw_response.exchange( - run_id="", - code="otp_abc123xyz", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_submit(self, async_client: AsyncKernel) -> None: - run = await async_client.agents.auth.runs.submit( - run_id="run_id", - field_values={ - "email": "user@example.com", - "password": "********", - }, - ) - assert_matches_type(AgentAuthSubmitResponse, run, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_submit(self, async_client: AsyncKernel) -> None: - response = await async_client.agents.auth.runs.with_raw_response.submit( - run_id="run_id", - field_values={ - "email": "user@example.com", - "password": "********", - }, - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - run = await response.parse() - assert_matches_type(AgentAuthSubmitResponse, run, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_submit(self, async_client: AsyncKernel) -> None: - async with async_client.agents.auth.runs.with_streaming_response.submit( - run_id="run_id", - field_values={ - "email": "user@example.com", - "password": "********", - }, - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - run = await response.parse() - assert_matches_type(AgentAuthSubmitResponse, run, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_submit(self, async_client: AsyncKernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `run_id` but received ''"): - await async_client.agents.auth.runs.with_raw_response.submit( - run_id="", - field_values={ - "email": "user@example.com", - "password": "********", - }, - ) diff --git a/tests/api_resources/agents/test_auth.py b/tests/api_resources/agents/test_auth.py deleted file mode 100644 index 32d2784f..00000000 --- a/tests/api_resources/agents/test_auth.py +++ /dev/null @@ -1,120 +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 kernel import Kernel, AsyncKernel -from tests.utils import assert_matches_type -from kernel.types.agents import AgentAuthStartResponse - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestAuth: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_start(self, client: Kernel) -> None: - auth = client.agents.auth.start( - profile_name="auth-abc123", - target_domain="doordash.com", - ) - assert_matches_type(AgentAuthStartResponse, auth, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_start_with_all_params(self, client: Kernel) -> None: - auth = client.agents.auth.start( - profile_name="auth-abc123", - target_domain="doordash.com", - app_logo_url="https://example.com/logo.png", - proxy={"proxy_id": "proxy_id"}, - ) - assert_matches_type(AgentAuthStartResponse, auth, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_start(self, client: Kernel) -> None: - response = client.agents.auth.with_raw_response.start( - profile_name="auth-abc123", - target_domain="doordash.com", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - auth = response.parse() - assert_matches_type(AgentAuthStartResponse, auth, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_start(self, client: Kernel) -> None: - with client.agents.auth.with_streaming_response.start( - profile_name="auth-abc123", - target_domain="doordash.com", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - auth = response.parse() - assert_matches_type(AgentAuthStartResponse, auth, path=["response"]) - - assert cast(Any, response.is_closed) is True - - -class TestAsyncAuth: - 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_start(self, async_client: AsyncKernel) -> None: - auth = await async_client.agents.auth.start( - profile_name="auth-abc123", - target_domain="doordash.com", - ) - assert_matches_type(AgentAuthStartResponse, auth, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_start_with_all_params(self, async_client: AsyncKernel) -> None: - auth = await async_client.agents.auth.start( - profile_name="auth-abc123", - target_domain="doordash.com", - app_logo_url="https://example.com/logo.png", - proxy={"proxy_id": "proxy_id"}, - ) - assert_matches_type(AgentAuthStartResponse, auth, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_start(self, async_client: AsyncKernel) -> None: - response = await async_client.agents.auth.with_raw_response.start( - profile_name="auth-abc123", - target_domain="doordash.com", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - auth = await response.parse() - assert_matches_type(AgentAuthStartResponse, auth, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_start(self, async_client: AsyncKernel) -> None: - async with async_client.agents.auth.with_streaming_response.start( - profile_name="auth-abc123", - target_domain="doordash.com", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - auth = await response.parse() - assert_matches_type(AgentAuthStartResponse, auth, path=["response"]) - - assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py new file mode 100644 index 00000000..6a8f164e --- /dev/null +++ b/tests/api_resources/test_browser_pools.py @@ -0,0 +1,856 @@ +# 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 kernel import Kernel, AsyncKernel +from tests.utils import assert_matches_type +from kernel.types import ( + BrowserPool, + BrowserPoolListResponse, + BrowserPoolAcquireResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestBrowserPools: + 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: Kernel) -> None: + browser_pool = client.browser_pools.create( + size=10, + ) + assert_matches_type(BrowserPool, browser_pool, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_create_with_all_params(self, client: Kernel) -> None: + browser_pool = client.browser_pools.create( + size=10, + extensions=[ + { + "id": "id", + "name": "name", + } + ], + fill_rate_per_minute=0, + headless=False, + kiosk_mode=True, + name="my-pool", + profile={ + "id": "id", + "name": "name", + "save_changes": True, + }, + proxy_id="proxy_id", + stealth=True, + timeout_seconds=60, + viewport={ + "height": 800, + "width": 1280, + "refresh_rate": 60, + }, + ) + assert_matches_type(BrowserPool, browser_pool, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_create(self, client: Kernel) -> None: + response = client.browser_pools.with_raw_response.create( + size=10, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser_pool = response.parse() + assert_matches_type(BrowserPool, browser_pool, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_create(self, client: Kernel) -> None: + with client.browser_pools.with_streaming_response.create( + size=10, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser_pool = response.parse() + assert_matches_type(BrowserPool, browser_pool, 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: Kernel) -> None: + browser_pool = client.browser_pools.retrieve( + "id_or_name", + ) + assert_matches_type(BrowserPool, browser_pool, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: Kernel) -> None: + response = client.browser_pools.with_raw_response.retrieve( + "id_or_name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser_pool = response.parse() + assert_matches_type(BrowserPool, browser_pool, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: Kernel) -> None: + with client.browser_pools.with_streaming_response.retrieve( + "id_or_name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser_pool = response.parse() + assert_matches_type(BrowserPool, browser_pool, 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: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): + client.browser_pools.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_update(self, client: Kernel) -> None: + browser_pool = client.browser_pools.update( + id_or_name="id_or_name", + size=10, + ) + assert_matches_type(BrowserPool, browser_pool, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_update_with_all_params(self, client: Kernel) -> None: + browser_pool = client.browser_pools.update( + id_or_name="id_or_name", + size=10, + discard_all_idle=False, + extensions=[ + { + "id": "id", + "name": "name", + } + ], + fill_rate_per_minute=0, + headless=False, + kiosk_mode=True, + name="my-pool", + profile={ + "id": "id", + "name": "name", + "save_changes": True, + }, + proxy_id="proxy_id", + stealth=True, + timeout_seconds=60, + viewport={ + "height": 800, + "width": 1280, + "refresh_rate": 60, + }, + ) + assert_matches_type(BrowserPool, browser_pool, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_update(self, client: Kernel) -> None: + response = client.browser_pools.with_raw_response.update( + id_or_name="id_or_name", + size=10, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser_pool = response.parse() + assert_matches_type(BrowserPool, browser_pool, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_update(self, client: Kernel) -> None: + with client.browser_pools.with_streaming_response.update( + id_or_name="id_or_name", + size=10, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser_pool = response.parse() + assert_matches_type(BrowserPool, browser_pool, 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: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): + client.browser_pools.with_raw_response.update( + id_or_name="", + size=10, + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_list(self, client: Kernel) -> None: + browser_pool = client.browser_pools.list() + assert_matches_type(BrowserPoolListResponse, browser_pool, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_list(self, client: Kernel) -> None: + response = client.browser_pools.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser_pool = response.parse() + assert_matches_type(BrowserPoolListResponse, browser_pool, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_list(self, client: Kernel) -> None: + with client.browser_pools.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser_pool = response.parse() + assert_matches_type(BrowserPoolListResponse, browser_pool, 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: Kernel) -> None: + browser_pool = client.browser_pools.delete( + id_or_name="id_or_name", + ) + assert browser_pool is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_delete_with_all_params(self, client: Kernel) -> None: + browser_pool = client.browser_pools.delete( + id_or_name="id_or_name", + force=True, + ) + assert browser_pool is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_delete(self, client: Kernel) -> None: + response = client.browser_pools.with_raw_response.delete( + id_or_name="id_or_name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser_pool = response.parse() + assert browser_pool is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_delete(self, client: Kernel) -> None: + with client.browser_pools.with_streaming_response.delete( + id_or_name="id_or_name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser_pool = response.parse() + assert browser_pool 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: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): + client.browser_pools.with_raw_response.delete( + id_or_name="", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_acquire(self, client: Kernel) -> None: + browser_pool = client.browser_pools.acquire( + id_or_name="id_or_name", + ) + assert_matches_type(BrowserPoolAcquireResponse, browser_pool, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_acquire_with_all_params(self, client: Kernel) -> None: + browser_pool = client.browser_pools.acquire( + id_or_name="id_or_name", + acquire_timeout_seconds=0, + ) + assert_matches_type(BrowserPoolAcquireResponse, browser_pool, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_acquire(self, client: Kernel) -> None: + response = client.browser_pools.with_raw_response.acquire( + id_or_name="id_or_name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser_pool = response.parse() + assert_matches_type(BrowserPoolAcquireResponse, browser_pool, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_acquire(self, client: Kernel) -> None: + with client.browser_pools.with_streaming_response.acquire( + id_or_name="id_or_name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser_pool = response.parse() + assert_matches_type(BrowserPoolAcquireResponse, browser_pool, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_acquire(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): + client.browser_pools.with_raw_response.acquire( + id_or_name="", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_flush(self, client: Kernel) -> None: + browser_pool = client.browser_pools.flush( + "id_or_name", + ) + assert browser_pool is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_flush(self, client: Kernel) -> None: + response = client.browser_pools.with_raw_response.flush( + "id_or_name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser_pool = response.parse() + assert browser_pool is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_flush(self, client: Kernel) -> None: + with client.browser_pools.with_streaming_response.flush( + "id_or_name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser_pool = response.parse() + assert browser_pool is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_flush(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): + client.browser_pools.with_raw_response.flush( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_release(self, client: Kernel) -> None: + browser_pool = client.browser_pools.release( + id_or_name="id_or_name", + session_id="ts8iy3sg25ibheguyni2lg9t", + ) + assert browser_pool is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_release_with_all_params(self, client: Kernel) -> None: + browser_pool = client.browser_pools.release( + id_or_name="id_or_name", + session_id="ts8iy3sg25ibheguyni2lg9t", + reuse=False, + ) + assert browser_pool is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_release(self, client: Kernel) -> None: + response = client.browser_pools.with_raw_response.release( + id_or_name="id_or_name", + session_id="ts8iy3sg25ibheguyni2lg9t", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser_pool = response.parse() + assert browser_pool is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_release(self, client: Kernel) -> None: + with client.browser_pools.with_streaming_response.release( + id_or_name="id_or_name", + session_id="ts8iy3sg25ibheguyni2lg9t", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser_pool = response.parse() + assert browser_pool is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_release(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): + client.browser_pools.with_raw_response.release( + id_or_name="", + session_id="ts8iy3sg25ibheguyni2lg9t", + ) + + +class TestAsyncBrowserPools: + 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: AsyncKernel) -> None: + browser_pool = await async_client.browser_pools.create( + size=10, + ) + assert_matches_type(BrowserPool, browser_pool, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: + browser_pool = await async_client.browser_pools.create( + size=10, + extensions=[ + { + "id": "id", + "name": "name", + } + ], + fill_rate_per_minute=0, + headless=False, + kiosk_mode=True, + name="my-pool", + profile={ + "id": "id", + "name": "name", + "save_changes": True, + }, + proxy_id="proxy_id", + stealth=True, + timeout_seconds=60, + viewport={ + "height": 800, + "width": 1280, + "refresh_rate": 60, + }, + ) + assert_matches_type(BrowserPool, browser_pool, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_create(self, async_client: AsyncKernel) -> None: + response = await async_client.browser_pools.with_raw_response.create( + size=10, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser_pool = await response.parse() + assert_matches_type(BrowserPool, browser_pool, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: + async with async_client.browser_pools.with_streaming_response.create( + size=10, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser_pool = await response.parse() + assert_matches_type(BrowserPool, browser_pool, 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: AsyncKernel) -> None: + browser_pool = await async_client.browser_pools.retrieve( + "id_or_name", + ) + assert_matches_type(BrowserPool, browser_pool, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: + response = await async_client.browser_pools.with_raw_response.retrieve( + "id_or_name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser_pool = await response.parse() + assert_matches_type(BrowserPool, browser_pool, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: + async with async_client.browser_pools.with_streaming_response.retrieve( + "id_or_name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser_pool = await response.parse() + assert_matches_type(BrowserPool, browser_pool, 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: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): + await async_client.browser_pools.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_update(self, async_client: AsyncKernel) -> None: + browser_pool = await async_client.browser_pools.update( + id_or_name="id_or_name", + size=10, + ) + assert_matches_type(BrowserPool, browser_pool, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> None: + browser_pool = await async_client.browser_pools.update( + id_or_name="id_or_name", + size=10, + discard_all_idle=False, + extensions=[ + { + "id": "id", + "name": "name", + } + ], + fill_rate_per_minute=0, + headless=False, + kiosk_mode=True, + name="my-pool", + profile={ + "id": "id", + "name": "name", + "save_changes": True, + }, + proxy_id="proxy_id", + stealth=True, + timeout_seconds=60, + viewport={ + "height": 800, + "width": 1280, + "refresh_rate": 60, + }, + ) + assert_matches_type(BrowserPool, browser_pool, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_update(self, async_client: AsyncKernel) -> None: + response = await async_client.browser_pools.with_raw_response.update( + id_or_name="id_or_name", + size=10, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser_pool = await response.parse() + assert_matches_type(BrowserPool, browser_pool, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_update(self, async_client: AsyncKernel) -> None: + async with async_client.browser_pools.with_streaming_response.update( + id_or_name="id_or_name", + size=10, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser_pool = await response.parse() + assert_matches_type(BrowserPool, browser_pool, 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: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): + await async_client.browser_pools.with_raw_response.update( + id_or_name="", + size=10, + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncKernel) -> None: + browser_pool = await async_client.browser_pools.list() + assert_matches_type(BrowserPoolListResponse, browser_pool, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncKernel) -> None: + response = await async_client.browser_pools.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser_pool = await response.parse() + assert_matches_type(BrowserPoolListResponse, browser_pool, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: + async with async_client.browser_pools.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser_pool = await response.parse() + assert_matches_type(BrowserPoolListResponse, browser_pool, 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: AsyncKernel) -> None: + browser_pool = await async_client.browser_pools.delete( + id_or_name="id_or_name", + ) + assert browser_pool is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_delete_with_all_params(self, async_client: AsyncKernel) -> None: + browser_pool = await async_client.browser_pools.delete( + id_or_name="id_or_name", + force=True, + ) + assert browser_pool is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: + response = await async_client.browser_pools.with_raw_response.delete( + id_or_name="id_or_name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser_pool = await response.parse() + assert browser_pool is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None: + async with async_client.browser_pools.with_streaming_response.delete( + id_or_name="id_or_name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser_pool = await response.parse() + assert browser_pool 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: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): + await async_client.browser_pools.with_raw_response.delete( + id_or_name="", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_acquire(self, async_client: AsyncKernel) -> None: + browser_pool = await async_client.browser_pools.acquire( + id_or_name="id_or_name", + ) + assert_matches_type(BrowserPoolAcquireResponse, browser_pool, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_acquire_with_all_params(self, async_client: AsyncKernel) -> None: + browser_pool = await async_client.browser_pools.acquire( + id_or_name="id_or_name", + acquire_timeout_seconds=0, + ) + assert_matches_type(BrowserPoolAcquireResponse, browser_pool, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_acquire(self, async_client: AsyncKernel) -> None: + response = await async_client.browser_pools.with_raw_response.acquire( + id_or_name="id_or_name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser_pool = await response.parse() + assert_matches_type(BrowserPoolAcquireResponse, browser_pool, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_acquire(self, async_client: AsyncKernel) -> None: + async with async_client.browser_pools.with_streaming_response.acquire( + id_or_name="id_or_name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser_pool = await response.parse() + assert_matches_type(BrowserPoolAcquireResponse, browser_pool, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_acquire(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): + await async_client.browser_pools.with_raw_response.acquire( + id_or_name="", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_flush(self, async_client: AsyncKernel) -> None: + browser_pool = await async_client.browser_pools.flush( + "id_or_name", + ) + assert browser_pool is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_flush(self, async_client: AsyncKernel) -> None: + response = await async_client.browser_pools.with_raw_response.flush( + "id_or_name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser_pool = await response.parse() + assert browser_pool is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_flush(self, async_client: AsyncKernel) -> None: + async with async_client.browser_pools.with_streaming_response.flush( + "id_or_name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser_pool = await response.parse() + assert browser_pool is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_flush(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): + await async_client.browser_pools.with_raw_response.flush( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_release(self, async_client: AsyncKernel) -> None: + browser_pool = await async_client.browser_pools.release( + id_or_name="id_or_name", + session_id="ts8iy3sg25ibheguyni2lg9t", + ) + assert browser_pool is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_release_with_all_params(self, async_client: AsyncKernel) -> None: + browser_pool = await async_client.browser_pools.release( + id_or_name="id_or_name", + session_id="ts8iy3sg25ibheguyni2lg9t", + reuse=False, + ) + assert browser_pool is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_release(self, async_client: AsyncKernel) -> None: + response = await async_client.browser_pools.with_raw_response.release( + id_or_name="id_or_name", + session_id="ts8iy3sg25ibheguyni2lg9t", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser_pool = await response.parse() + assert browser_pool is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_release(self, async_client: AsyncKernel) -> None: + async with async_client.browser_pools.with_streaming_response.release( + id_or_name="id_or_name", + session_id="ts8iy3sg25ibheguyni2lg9t", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser_pool = await response.parse() + assert browser_pool is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_release(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): + await async_client.browser_pools.with_raw_response.release( + id_or_name="", + session_id="ts8iy3sg25ibheguyni2lg9t", + ) From 05eca5166f1ee8d4c72679ae560a84c00ae42538 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 22:14:13 +0000 Subject: [PATCH 225/448] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index c32f96db..25e77f1e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 74 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-340c8f009b71922347d4c238c8715cd752c8965abfa12cbb1ffabe35edc338a8.yml -openapi_spec_hash: efc13ab03ef89cc07333db8ab5345f31 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-3db06d1628149b5ea8303f1c72250664dfd7cb4a14ceb6102f1ae6e85c92c038.yml +openapi_spec_hash: e5b3da2da328eb26d2a70e2521744c62 config_hash: a4124701ae0a474e580d7416adbcfb00 From 00fc4a2c98d2b664c05ab3499b6bb978788003b7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 22:16:10 +0000 Subject: [PATCH 226/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 0c2ecec6..86b0e83d 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.20.0" + ".": "0.21.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 71c4c9aa..f4bbe655 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.20.0" +version = "0.21.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 1a28cba2..1bd01d63 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.20.0" # x-release-please-version +__version__ = "0.21.0" # x-release-please-version From 93e4807b87ef23aee645be154ece9f568060b135 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 04:03:18 +0000 Subject: [PATCH 227/448] chore: update lockfile --- pyproject.toml | 14 +++--- requirements-dev.lock | 108 +++++++++++++++++++++++------------------- requirements.lock | 31 ++++++------ 3 files changed, 83 insertions(+), 70 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f4bbe655..59d67db7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,14 +7,16 @@ license = "Apache-2.0" authors = [ { name = "Kernel", email = "" }, ] + dependencies = [ - "httpx>=0.23.0, <1", - "pydantic>=1.9.0, <3", - "typing-extensions>=4.10, <5", - "anyio>=3.5.0, <5", - "distro>=1.7.0, <2", - "sniffio", + "httpx>=0.23.0, <1", + "pydantic>=1.9.0, <3", + "typing-extensions>=4.10, <5", + "anyio>=3.5.0, <5", + "distro>=1.7.0, <2", + "sniffio", ] + requires-python = ">= 3.9" classifiers = [ "Typing :: Typed", diff --git a/requirements-dev.lock b/requirements-dev.lock index b435fc70..7643dfba 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -12,40 +12,45 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.12.8 +aiohttp==3.13.2 # via httpx-aiohttp # via kernel -aiosignal==1.3.2 +aiosignal==1.4.0 # via aiohttp -annotated-types==0.6.0 +annotated-types==0.7.0 # via pydantic -anyio==4.4.0 +anyio==4.12.0 # via httpx # via kernel -argcomplete==3.1.2 +argcomplete==3.6.3 # via nox async-timeout==5.0.1 # via aiohttp -attrs==25.3.0 +attrs==25.4.0 # via aiohttp -certifi==2023.7.22 + # via nox +backports-asyncio-runner==1.2.0 + # via pytest-asyncio +certifi==2025.11.12 # via httpcore # via httpx -colorlog==6.7.0 +colorlog==6.10.1 + # via nox +dependency-groups==1.3.1 # via nox -dirty-equals==0.6.0 -distlib==0.3.7 +dirty-equals==0.11 +distlib==0.4.0 # via virtualenv -distro==1.8.0 +distro==1.9.0 # via kernel -exceptiongroup==1.2.2 +exceptiongroup==1.3.1 # via anyio # via pytest -execnet==2.1.1 +execnet==2.1.2 # via pytest-xdist -filelock==3.12.4 +filelock==3.19.1 # via virtualenv -frozenlist==1.6.2 +frozenlist==1.8.0 # via aiohttp # via aiosignal h11==0.16.0 @@ -58,82 +63,87 @@ httpx==0.28.1 # via respx httpx-aiohttp==0.1.9 # via kernel -idna==3.4 +humanize==4.13.0 + # via nox +idna==3.11 # via anyio # via httpx # via yarl -importlib-metadata==7.0.0 -iniconfig==2.0.0 +importlib-metadata==8.7.0 +iniconfig==2.1.0 # via pytest markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -multidict==6.4.4 +multidict==6.7.0 # via aiohttp # via yarl mypy==1.17.0 -mypy-extensions==1.0.0 +mypy-extensions==1.1.0 # via mypy -nodeenv==1.8.0 +nodeenv==1.9.1 # via pyright -nox==2023.4.22 -packaging==23.2 +nox==2025.11.12 +packaging==25.0 + # via dependency-groups # via nox # via pytest pathspec==0.12.1 # via mypy -platformdirs==3.11.0 +platformdirs==4.4.0 # via virtualenv -pluggy==1.5.0 +pluggy==1.6.0 # via pytest -propcache==0.3.1 +propcache==0.4.1 # via aiohttp # via yarl -pydantic==2.11.9 +pydantic==2.12.5 # via kernel -pydantic-core==2.33.2 +pydantic-core==2.41.5 # via pydantic -pygments==2.18.0 +pygments==2.19.2 + # via pytest # via rich pyright==1.1.399 -pytest==8.3.3 +pytest==8.4.2 # via pytest-asyncio # via pytest-xdist -pytest-asyncio==0.24.0 -pytest-xdist==3.7.0 -python-dateutil==2.8.2 +pytest-asyncio==1.2.0 +pytest-xdist==3.8.0 +python-dateutil==2.9.0.post0 # via time-machine -pytz==2023.3.post1 - # via dirty-equals respx==0.22.0 -rich==13.7.1 -ruff==0.9.4 -setuptools==68.2.2 - # via nodeenv -six==1.16.0 +rich==14.2.0 +ruff==0.14.7 +six==1.17.0 # via python-dateutil -sniffio==1.3.0 - # via anyio +sniffio==1.3.1 # via kernel -time-machine==2.9.0 -tomli==2.0.2 +time-machine==2.19.0 +tomli==2.3.0 + # via dependency-groups # via mypy + # via nox # via pytest -typing-extensions==4.12.2 +typing-extensions==4.15.0 + # via aiosignal # via anyio + # via exceptiongroup # via kernel # via multidict # via mypy # via pydantic # via pydantic-core # via pyright + # via pytest-asyncio # via typing-inspection -typing-inspection==0.4.1 + # via virtualenv +typing-inspection==0.4.2 # via pydantic -virtualenv==20.24.5 +virtualenv==20.35.4 # via nox -yarl==1.20.0 +yarl==1.22.0 # via aiohttp -zipp==3.17.0 +zipp==3.23.0 # via importlib-metadata diff --git a/requirements.lock b/requirements.lock index 646e8cda..bbfe2b35 100644 --- a/requirements.lock +++ b/requirements.lock @@ -12,28 +12,28 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.12.8 +aiohttp==3.13.2 # via httpx-aiohttp # via kernel -aiosignal==1.3.2 +aiosignal==1.4.0 # via aiohttp -annotated-types==0.6.0 +annotated-types==0.7.0 # via pydantic -anyio==4.4.0 +anyio==4.12.0 # via httpx # via kernel async-timeout==5.0.1 # via aiohttp -attrs==25.3.0 +attrs==25.4.0 # via aiohttp -certifi==2023.7.22 +certifi==2025.11.12 # via httpcore # via httpx -distro==1.8.0 +distro==1.9.0 # via kernel -exceptiongroup==1.2.2 +exceptiongroup==1.3.1 # via anyio -frozenlist==1.6.2 +frozenlist==1.8.0 # via aiohttp # via aiosignal h11==0.16.0 @@ -45,25 +45,26 @@ httpx==0.28.1 # via kernel httpx-aiohttp==0.1.9 # via kernel -idna==3.4 +idna==3.11 # via anyio # via httpx # via yarl -multidict==6.4.4 +multidict==6.7.0 # via aiohttp # via yarl -propcache==0.3.1 +propcache==0.4.1 # via aiohttp # via yarl pydantic==2.12.5 # via kernel pydantic-core==2.41.5 # via pydantic -sniffio==1.3.0 - # via anyio +sniffio==1.3.1 # via kernel typing-extensions==4.15.0 + # via aiosignal # via anyio + # via exceptiongroup # via kernel # via multidict # via pydantic @@ -71,5 +72,5 @@ typing-extensions==4.15.0 # via typing-inspection typing-inspection==0.4.2 # via pydantic -yarl==1.20.0 +yarl==1.22.0 # via aiohttp From caae377c7ef63805596286d9184ce1faffb93b99 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 04:14:36 +0000 Subject: [PATCH 228/448] chore(docs): use environment variables for authentication in code snippets --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 699f3c26..d5905eaf 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ pip install kernel[aiohttp] Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: ```python +import os import asyncio from kernel import DefaultAioHttpClient from kernel import AsyncKernel @@ -94,7 +95,7 @@ from kernel import AsyncKernel async def main() -> None: async with AsyncKernel( - api_key="My API Key", + api_key=os.environ.get("KERNEL_API_KEY"), # This is the default and can be omitted http_client=DefaultAioHttpClient(), ) as client: browser = await client.browsers.create( From 0eec0dbf8a1b740cb0eee6ec6268eb42b25c1ec0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 5 Dec 2025 20:08:26 +0000 Subject: [PATCH 229/448] feat: Add `async_timeout_seconds` to PostInvocations --- .stats.yml | 4 ++-- src/kernel/resources/invocations.py | 10 ++++++++++ src/kernel/types/invocation_create_params.py | 6 ++++++ tests/api_resources/test_invocations.py | 2 ++ 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 25e77f1e..7792bb0d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 74 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-3db06d1628149b5ea8303f1c72250664dfd7cb4a14ceb6102f1ae6e85c92c038.yml -openapi_spec_hash: e5b3da2da328eb26d2a70e2521744c62 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-003e9afa15f0765009d2c7d34e8eb62268d818e628e3c84361b21138e30cc423.yml +openapi_spec_hash: c1b8309f60385bf2b02d245363ca47c1 config_hash: a4124701ae0a474e580d7416adbcfb00 diff --git a/src/kernel/resources/invocations.py b/src/kernel/resources/invocations.py index 4c7e7816..fa808dd0 100644 --- a/src/kernel/resources/invocations.py +++ b/src/kernel/resources/invocations.py @@ -57,6 +57,7 @@ def create( app_name: str, version: str, async_: bool | Omit = omit, + async_timeout_seconds: int | Omit = omit, payload: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -78,6 +79,9 @@ def create( async_: If true, invoke asynchronously. When set, the API responds 202 Accepted with status "queued". + async_timeout_seconds: Timeout in seconds for async invocations (min 10, max 3600). Only applies when + async is true. + payload: Input data for the action, sent as a JSON string. extra_headers: Send extra headers @@ -96,6 +100,7 @@ def create( "app_name": app_name, "version": version, "async_": async_, + "async_timeout_seconds": async_timeout_seconds, "payload": payload, }, invocation_create_params.InvocationCreateParams, @@ -370,6 +375,7 @@ async def create( app_name: str, version: str, async_: bool | Omit = omit, + async_timeout_seconds: int | Omit = omit, payload: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -391,6 +397,9 @@ async def create( async_: If true, invoke asynchronously. When set, the API responds 202 Accepted with status "queued". + async_timeout_seconds: Timeout in seconds for async invocations (min 10, max 3600). Only applies when + async is true. + payload: Input data for the action, sent as a JSON string. extra_headers: Send extra headers @@ -409,6 +418,7 @@ async def create( "app_name": app_name, "version": version, "async_": async_, + "async_timeout_seconds": async_timeout_seconds, "payload": payload, }, invocation_create_params.InvocationCreateParams, diff --git a/src/kernel/types/invocation_create_params.py b/src/kernel/types/invocation_create_params.py index 1d6bc64e..288656a6 100644 --- a/src/kernel/types/invocation_create_params.py +++ b/src/kernel/types/invocation_create_params.py @@ -25,5 +25,11 @@ class InvocationCreateParams(TypedDict, total=False): When set, the API responds 202 Accepted with status "queued". """ + async_timeout_seconds: int + """Timeout in seconds for async invocations (min 10, max 3600). + + Only applies when async is true. + """ + payload: str """Input data for the action, sent as a JSON string.""" diff --git a/tests/api_resources/test_invocations.py b/tests/api_resources/test_invocations.py index d36ea25a..40c05453 100644 --- a/tests/api_resources/test_invocations.py +++ b/tests/api_resources/test_invocations.py @@ -41,6 +41,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: app_name="my-app", version="1.0.0", async_=True, + async_timeout_seconds=600, payload='{"data":"example input"}', ) assert_matches_type(InvocationCreateResponse, invocation, path=["response"]) @@ -332,6 +333,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> app_name="my-app", version="1.0.0", async_=True, + async_timeout_seconds=600, payload='{"data":"example input"}', ) assert_matches_type(InvocationCreateResponse, invocation, path=["response"]) From 138a1c43f86957c4beff633fb497518b4e25cc4a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 5 Dec 2025 20:12:06 +0000 Subject: [PATCH 230/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 86b0e83d..cb9d2541 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.21.0" + ".": "0.22.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 59d67db7..facf9996 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.21.0" +version = "0.22.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 1bd01d63..c911504a 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.21.0" # x-release-please-version +__version__ = "0.22.0" # x-release-please-version From 126b604055c9b8a1708d2d2428f91aabe09d1bb6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 5 Dec 2025 22:00:22 +0000 Subject: [PATCH 231/448] refactor(browser): remove persistence option UI --- .stats.yml | 6 +- README.md | 18 +++--- src/kernel/resources/browsers/browsers.py | 61 +++++++++++------- src/kernel/types/browser_create_params.py | 11 ++-- src/kernel/types/browser_create_response.py | 2 +- src/kernel/types/browser_list_response.py | 2 +- src/kernel/types/browser_persistence.py | 2 +- src/kernel/types/browser_persistence_param.py | 2 +- .../types/browser_pool_acquire_response.py | 2 +- src/kernel/types/browser_retrieve_response.py | 2 +- tests/api_resources/test_browsers.py | 64 +++++++++++-------- 11 files changed, 96 insertions(+), 76 deletions(-) diff --git a/.stats.yml b/.stats.yml index 7792bb0d..a0095f11 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 74 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-003e9afa15f0765009d2c7d34e8eb62268d818e628e3c84361b21138e30cc423.yml -openapi_spec_hash: c1b8309f60385bf2b02d245363ca47c1 -config_hash: a4124701ae0a474e580d7416adbcfb00 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-cbef7e4fef29ad40af5c767aceb762ee68811c2287f255c05d2ee44a9a9247a2.yml +openapi_spec_hash: 467e61e072773ec9f2d49c7dd3de8c2f +config_hash: 6dbe88d2ba9df1ec46cedbfdb7d00000 diff --git a/README.md b/README.md index d5905eaf..6d1dd19b 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ client = Kernel( ) browser = client.browsers.create( - persistence={"id": "browser-for-user-1234"}, + stealth=True, ) print(browser.session_id) ``` @@ -63,7 +63,7 @@ client = AsyncKernel( async def main() -> None: browser = await client.browsers.create( - persistence={"id": "browser-for-user-1234"}, + stealth=True, ) print(browser.session_id) @@ -99,7 +99,7 @@ async def main() -> None: http_client=DefaultAioHttpClient(), ) as client: browser = await client.browsers.create( - persistence={"id": "browser-for-user-1234"}, + stealth=True, ) print(browser.session_id) @@ -242,7 +242,7 @@ client = Kernel() try: client.browsers.create( - persistence={"id": "browser-for-user-1234"}, + stealth=True, ) except kernel.APIConnectionError as e: print("The server could not be reached") @@ -287,7 +287,7 @@ client = Kernel( # Or, configure per-request: client.with_options(max_retries=5).browsers.create( - persistence={"id": "browser-for-user-1234"}, + stealth=True, ) ``` @@ -312,7 +312,7 @@ client = Kernel( # Override per-request: client.with_options(timeout=5.0).browsers.create( - persistence={"id": "browser-for-user-1234"}, + stealth=True, ) ``` @@ -355,9 +355,7 @@ from kernel import Kernel client = Kernel() response = client.browsers.with_raw_response.create( - persistence={ - "id": "browser-for-user-1234" - }, + stealth=True, ) print(response.headers.get('X-My-Header')) @@ -377,7 +375,7 @@ To stream the response body, use `.with_streaming_response` instead, which requi ```python with client.browsers.with_streaming_response.create( - persistence={"id": "browser-for-user-1234"}, + stealth=True, ) as response: print(response.headers.get("X-My-Header")) diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index f41b46ad..30888da0 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -2,6 +2,7 @@ from __future__ import annotations +import typing_extensions from typing import Mapping, Iterable, cast import httpx @@ -161,7 +162,7 @@ def create( kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live view. - persistence: Optional persistence configuration for the browser session. + persistence: DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead. profile: Profile selection for the browser session. Provide either id or name. If specified, the matching profile will be loaded into the browser session. @@ -174,11 +175,10 @@ def create( mechanisms. timeout_seconds: The number of seconds of inactivity before the browser session is terminated. - Only applicable to non-persistent browsers. Activity includes CDP connections - and live view connections. Defaults to 60 seconds. Minimum allowed is 10 - seconds. Maximum allowed is 259200 (72 hours). We check for inactivity every 5 - seconds, so the actual timeout behavior you will see is +/- 5 seconds around the - specified value. + Activity includes CDP connections and live view connections. Defaults to 60 + seconds. Minimum allowed is 10 seconds. Maximum allowed is 259200 (72 hours). We + check for inactivity every 5 seconds, so the actual timeout behavior you will + see is +/- 5 seconds around the specified value. viewport: Initial browser window size in pixels with optional refresh rate. If omitted, image defaults apply (commonly 1024x768@60). Only specific viewport @@ -307,6 +307,7 @@ def list( model=BrowserListResponse, ) + @typing_extensions.deprecated("deprecated") def delete( self, *, @@ -318,8 +319,10 @@ def delete( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: - """ - Delete a persistent browser session by its persistent_id. + """DEPRECATED: Use DELETE /browsers/{id} instead. + + Delete a persistent browser + session by its persistent_id. Args: persistent_id: Persistent browser identifier @@ -504,7 +507,7 @@ async def create( kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live view. - persistence: Optional persistence configuration for the browser session. + persistence: DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead. profile: Profile selection for the browser session. Provide either id or name. If specified, the matching profile will be loaded into the browser session. @@ -517,11 +520,10 @@ async def create( mechanisms. timeout_seconds: The number of seconds of inactivity before the browser session is terminated. - Only applicable to non-persistent browsers. Activity includes CDP connections - and live view connections. Defaults to 60 seconds. Minimum allowed is 10 - seconds. Maximum allowed is 259200 (72 hours). We check for inactivity every 5 - seconds, so the actual timeout behavior you will see is +/- 5 seconds around the - specified value. + Activity includes CDP connections and live view connections. Defaults to 60 + seconds. Minimum allowed is 10 seconds. Maximum allowed is 259200 (72 hours). We + check for inactivity every 5 seconds, so the actual timeout behavior you will + see is +/- 5 seconds around the specified value. viewport: Initial browser window size in pixels with optional refresh rate. If omitted, image defaults apply (commonly 1024x768@60). Only specific viewport @@ -650,6 +652,7 @@ def list( model=BrowserListResponse, ) + @typing_extensions.deprecated("deprecated") async def delete( self, *, @@ -661,8 +664,10 @@ async def delete( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: - """ - Delete a persistent browser session by its persistent_id. + """DEPRECATED: Use DELETE /browsers/{id} instead. + + Delete a persistent browser + session by its persistent_id. Args: persistent_id: Persistent browser identifier @@ -784,8 +789,10 @@ def __init__(self, browsers: BrowsersResource) -> None: self.list = to_raw_response_wrapper( browsers.list, ) - self.delete = to_raw_response_wrapper( - browsers.delete, + self.delete = ( # pyright: ignore[reportDeprecated] + to_raw_response_wrapper( + browsers.delete, # pyright: ignore[reportDeprecated], + ) ) self.delete_by_id = to_raw_response_wrapper( browsers.delete_by_id, @@ -832,8 +839,10 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None: self.list = async_to_raw_response_wrapper( browsers.list, ) - self.delete = async_to_raw_response_wrapper( - browsers.delete, + self.delete = ( # pyright: ignore[reportDeprecated] + async_to_raw_response_wrapper( + browsers.delete, # pyright: ignore[reportDeprecated], + ) ) self.delete_by_id = async_to_raw_response_wrapper( browsers.delete_by_id, @@ -880,8 +889,10 @@ def __init__(self, browsers: BrowsersResource) -> None: self.list = to_streamed_response_wrapper( browsers.list, ) - self.delete = to_streamed_response_wrapper( - browsers.delete, + self.delete = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + browsers.delete, # pyright: ignore[reportDeprecated], + ) ) self.delete_by_id = to_streamed_response_wrapper( browsers.delete_by_id, @@ -928,8 +939,10 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None: self.list = async_to_streamed_response_wrapper( browsers.list, ) - self.delete = async_to_streamed_response_wrapper( - browsers.delete, + self.delete = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + browsers.delete, # pyright: ignore[reportDeprecated], + ) ) self.delete_by_id = async_to_streamed_response_wrapper( browsers.delete_by_id, diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index d1ff7907..4a5f4796 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -36,7 +36,7 @@ class BrowserCreateParams(TypedDict, total=False): """ persistence: BrowserPersistenceParam - """Optional persistence configuration for the browser session.""" + """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead.""" profile: BrowserProfile """Profile selection for the browser session. @@ -60,11 +60,10 @@ class BrowserCreateParams(TypedDict, total=False): timeout_seconds: int """The number of seconds of inactivity before the browser session is terminated. - Only applicable to non-persistent browsers. Activity includes CDP connections - and live view connections. Defaults to 60 seconds. Minimum allowed is 10 - seconds. Maximum allowed is 259200 (72 hours). We check for inactivity every 5 - seconds, so the actual timeout behavior you will see is +/- 5 seconds around the - specified value. + Activity includes CDP connections and live view connections. Defaults to 60 + seconds. Minimum allowed is 10 seconds. Maximum allowed is 259200 (72 hours). We + check for inactivity every 5 seconds, so the actual timeout behavior you will + see is +/- 5 seconds around the specified value. """ viewport: BrowserViewport diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index 0a5f33d4..344549e9 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -43,7 +43,7 @@ class BrowserCreateResponse(BaseModel): """Whether the browser session is running in kiosk mode.""" persistence: Optional[BrowserPersistence] = None - """Optional persistence configuration for the browser session.""" + """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead.""" profile: Optional[Profile] = None """Browser profile metadata.""" diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index d6397291..cfa2ce53 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -43,7 +43,7 @@ class BrowserListResponse(BaseModel): """Whether the browser session is running in kiosk mode.""" persistence: Optional[BrowserPersistence] = None - """Optional persistence configuration for the browser session.""" + """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead.""" profile: Optional[Profile] = None """Browser profile metadata.""" diff --git a/src/kernel/types/browser_persistence.py b/src/kernel/types/browser_persistence.py index 9c6bfc7f..5c362eee 100644 --- a/src/kernel/types/browser_persistence.py +++ b/src/kernel/types/browser_persistence.py @@ -7,4 +7,4 @@ class BrowserPersistence(BaseModel): id: str - """Unique identifier for the persistent browser session.""" + """DEPRECATED: Unique identifier for the persistent browser session.""" diff --git a/src/kernel/types/browser_persistence_param.py b/src/kernel/types/browser_persistence_param.py index b4832918..bbd9e483 100644 --- a/src/kernel/types/browser_persistence_param.py +++ b/src/kernel/types/browser_persistence_param.py @@ -9,4 +9,4 @@ class BrowserPersistenceParam(TypedDict, total=False): id: Required[str] - """Unique identifier for the persistent browser session.""" + """DEPRECATED: Unique identifier for the persistent browser session.""" diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py index 76ad037b..1bb69e8f 100644 --- a/src/kernel/types/browser_pool_acquire_response.py +++ b/src/kernel/types/browser_pool_acquire_response.py @@ -43,7 +43,7 @@ class BrowserPoolAcquireResponse(BaseModel): """Whether the browser session is running in kiosk mode.""" persistence: Optional[BrowserPersistence] = None - """Optional persistence configuration for the browser session.""" + """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead.""" profile: Optional[Profile] = None """Browser profile metadata.""" diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 111149b3..2b49d654 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -43,7 +43,7 @@ class BrowserRetrieveResponse(BaseModel): """Whether the browser session is running in kiosk mode.""" persistence: Optional[BrowserPersistence] = None - """Optional persistence configuration for the browser session.""" + """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead.""" profile: Optional[Profile] = None """Browser profile metadata.""" diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index c87fc3db..a7666565 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -16,6 +16,8 @@ ) from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination +# pyright: reportDeprecated=false + base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -163,17 +165,20 @@ def test_streaming_response_list(self, client: Kernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_delete(self, client: Kernel) -> None: - browser = client.browsers.delete( - persistent_id="persistent_id", - ) + with pytest.warns(DeprecationWarning): + browser = client.browsers.delete( + persistent_id="persistent_id", + ) + assert browser is None @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_delete(self, client: Kernel) -> None: - response = client.browsers.with_raw_response.delete( - persistent_id="persistent_id", - ) + with pytest.warns(DeprecationWarning): + response = client.browsers.with_raw_response.delete( + persistent_id="persistent_id", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -183,14 +188,15 @@ def test_raw_response_delete(self, client: Kernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_delete(self, client: Kernel) -> None: - with client.browsers.with_streaming_response.delete( - persistent_id="persistent_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + with client.browsers.with_streaming_response.delete( + persistent_id="persistent_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - browser = response.parse() - assert browser is None + browser = response.parse() + assert browser is None assert cast(Any, response.is_closed) is True @@ -449,17 +455,20 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_delete(self, async_client: AsyncKernel) -> None: - browser = await async_client.browsers.delete( - persistent_id="persistent_id", - ) + with pytest.warns(DeprecationWarning): + browser = await async_client.browsers.delete( + persistent_id="persistent_id", + ) + assert browser is None @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: - response = await async_client.browsers.with_raw_response.delete( - persistent_id="persistent_id", - ) + with pytest.warns(DeprecationWarning): + response = await async_client.browsers.with_raw_response.delete( + persistent_id="persistent_id", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -469,14 +478,15 @@ async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None: - async with async_client.browsers.with_streaming_response.delete( - persistent_id="persistent_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - browser = await response.parse() - assert browser is None + with pytest.warns(DeprecationWarning): + async with async_client.browsers.with_streaming_response.delete( + persistent_id="persistent_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser = await response.parse() + assert browser is None assert cast(Any, response.is_closed) is True From d0c62d8cc6b6a54c0f5ae9fd1e70d4b7d114c661 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 5 Dec 2025 22:56:08 +0000 Subject: [PATCH 232/448] feat: [wip] Browser pools polish pass --- .stats.yml | 4 +- src/kernel/resources/browser_pools.py | 44 +++++++++---------- src/kernel/resources/browsers/browsers.py | 20 ++++----- src/kernel/types/browser_create_params.py | 2 +- src/kernel/types/browser_create_response.py | 2 +- src/kernel/types/browser_list_response.py | 2 +- .../types/browser_pool_acquire_response.py | 2 +- .../types/browser_pool_create_params.py | 2 +- src/kernel/types/browser_pool_request.py | 2 +- .../types/browser_pool_update_params.py | 4 +- src/kernel/types/browser_retrieve_response.py | 2 +- 11 files changed, 43 insertions(+), 43 deletions(-) diff --git a/.stats.yml b/.stats.yml index a0095f11..44c807a5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 74 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-cbef7e4fef29ad40af5c767aceb762ee68811c2287f255c05d2ee44a9a9247a2.yml -openapi_spec_hash: 467e61e072773ec9f2d49c7dd3de8c2f +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-3a68acd8c46e121c66be5b4c30bb4e962967840ca0f31070905baa39635fbc2d.yml +openapi_spec_hash: 9453963fbb01de3e0afb462b16cdf115 config_hash: 6dbe88d2ba9df1ec46cedbfdb7d00000 diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py index d085d515..8c480ed5 100644 --- a/src/kernel/resources/browser_pools.py +++ b/src/kernel/resources/browser_pools.py @@ -106,11 +106,11 @@ def create( are destroyed. Defaults to 600 seconds if not specified viewport: Initial browser window size in pixels with optional refresh rate. If omitted, - image defaults apply (commonly 1024x768@60). Only specific viewport - configurations are supported. The server will reject unsupported combinations. - Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will - be automatically determined from the width and height if they match a supported + image defaults apply (1920x1080@25). Only specific viewport configurations are + supported. The server will reject unsupported combinations. Supported + resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, + 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will be + automatically determined from the width and height if they match a supported configuration exactly. Note: Higher resolutions may affect the responsiveness of live view browser @@ -209,7 +209,7 @@ def update( size: Number of browsers to create in the pool discard_all_idle: Whether to discard all idle browsers and rebuild the pool immediately. Defaults - to true. + to false. extensions: List of browser extensions to load into the session. Provide each by id or name. @@ -236,11 +236,11 @@ def update( are destroyed. Defaults to 600 seconds if not specified viewport: Initial browser window size in pixels with optional refresh rate. If omitted, - image defaults apply (commonly 1024x768@60). Only specific viewport - configurations are supported. The server will reject unsupported combinations. - Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will - be automatically determined from the width and height if they match a supported + image defaults apply (1920x1080@25). Only specific viewport configurations are + supported. The server will reject unsupported combinations. Supported + resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, + 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will be + automatically determined from the width and height if they match a supported configuration exactly. Note: Higher resolutions may affect the responsiveness of live view browser @@ -540,11 +540,11 @@ async def create( are destroyed. Defaults to 600 seconds if not specified viewport: Initial browser window size in pixels with optional refresh rate. If omitted, - image defaults apply (commonly 1024x768@60). Only specific viewport - configurations are supported. The server will reject unsupported combinations. - Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will - be automatically determined from the width and height if they match a supported + image defaults apply (1920x1080@25). Only specific viewport configurations are + supported. The server will reject unsupported combinations. Supported + resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, + 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will be + automatically determined from the width and height if they match a supported configuration exactly. Note: Higher resolutions may affect the responsiveness of live view browser @@ -643,7 +643,7 @@ async def update( size: Number of browsers to create in the pool discard_all_idle: Whether to discard all idle browsers and rebuild the pool immediately. Defaults - to true. + to false. extensions: List of browser extensions to load into the session. Provide each by id or name. @@ -670,11 +670,11 @@ async def update( are destroyed. Defaults to 600 seconds if not specified viewport: Initial browser window size in pixels with optional refresh rate. If omitted, - image defaults apply (commonly 1024x768@60). Only specific viewport - configurations are supported. The server will reject unsupported combinations. - Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will - be automatically determined from the width and height if they match a supported + image defaults apply (1920x1080@25). Only specific viewport configurations are + supported. The server will reject unsupported combinations. Supported + resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, + 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will be + automatically determined from the width and height if they match a supported configuration exactly. Note: Higher resolutions may affect the responsiveness of live view browser diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 30888da0..cbd17736 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -181,11 +181,11 @@ def create( see is +/- 5 seconds around the specified value. viewport: Initial browser window size in pixels with optional refresh rate. If omitted, - image defaults apply (commonly 1024x768@60). Only specific viewport - configurations are supported. The server will reject unsupported combinations. - Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will - be automatically determined from the width and height if they match a supported + image defaults apply (1920x1080@25). Only specific viewport configurations are + supported. The server will reject unsupported combinations. Supported + resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, + 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will be + automatically determined from the width and height if they match a supported configuration exactly. Note: Higher resolutions may affect the responsiveness of live view browser @@ -526,11 +526,11 @@ async def create( see is +/- 5 seconds around the specified value. viewport: Initial browser window size in pixels with optional refresh rate. If omitted, - image defaults apply (commonly 1024x768@60). Only specific viewport - configurations are supported. The server will reject unsupported combinations. - Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will - be automatically determined from the width and height if they match a supported + image defaults apply (1920x1080@25). Only specific viewport configurations are + supported. The server will reject unsupported combinations. Supported + resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, + 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will be + automatically determined from the width and height if they match a supported configuration exactly. Note: Higher resolutions may affect the responsiveness of live view browser diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index 4a5f4796..0818760c 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -69,7 +69,7 @@ class BrowserCreateParams(TypedDict, total=False): viewport: BrowserViewport """Initial browser window size in pixels with optional refresh rate. - If omitted, image defaults apply (commonly 1024x768@60). Only specific viewport + If omitted, image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index 344549e9..efff854f 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -54,7 +54,7 @@ class BrowserCreateResponse(BaseModel): viewport: Optional[BrowserViewport] = None """Initial browser window size in pixels with optional refresh rate. - If omitted, image defaults apply (commonly 1024x768@60). Only specific viewport + If omitted, image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index cfa2ce53..3ce26488 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -54,7 +54,7 @@ class BrowserListResponse(BaseModel): viewport: Optional[BrowserViewport] = None """Initial browser window size in pixels with optional refresh rate. - If omitted, image defaults apply (commonly 1024x768@60). Only specific viewport + If omitted, image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py index 1bb69e8f..4b70a87f 100644 --- a/src/kernel/types/browser_pool_acquire_response.py +++ b/src/kernel/types/browser_pool_acquire_response.py @@ -54,7 +54,7 @@ class BrowserPoolAcquireResponse(BaseModel): viewport: Optional[BrowserViewport] = None """Initial browser window size in pixels with optional refresh rate. - If omitted, image defaults apply (commonly 1024x768@60). Only specific viewport + If omitted, image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py index c7f87c64..6c8e815e 100644 --- a/src/kernel/types/browser_pool_create_params.py +++ b/src/kernel/types/browser_pool_create_params.py @@ -65,7 +65,7 @@ class BrowserPoolCreateParams(TypedDict, total=False): viewport: BrowserViewport """Initial browser window size in pixels with optional refresh rate. - If omitted, image defaults apply (commonly 1024x768@60). Only specific viewport + If omitted, image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will diff --git a/src/kernel/types/browser_pool_request.py b/src/kernel/types/browser_pool_request.py index c25b3a55..c54fad41 100644 --- a/src/kernel/types/browser_pool_request.py +++ b/src/kernel/types/browser_pool_request.py @@ -63,7 +63,7 @@ class BrowserPoolRequest(BaseModel): viewport: Optional[BrowserViewport] = None """Initial browser window size in pixels with optional refresh rate. - If omitted, image defaults apply (commonly 1024x768@60). Only specific viewport + If omitted, image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py index ed9a7e84..2cd3be71 100644 --- a/src/kernel/types/browser_pool_update_params.py +++ b/src/kernel/types/browser_pool_update_params.py @@ -19,7 +19,7 @@ class BrowserPoolUpdateParams(TypedDict, total=False): discard_all_idle: bool """Whether to discard all idle browsers and rebuild the pool immediately. - Defaults to true. + Defaults to false. """ extensions: Iterable[BrowserExtension] @@ -71,7 +71,7 @@ class BrowserPoolUpdateParams(TypedDict, total=False): viewport: BrowserViewport """Initial browser window size in pixels with optional refresh rate. - If omitted, image defaults apply (commonly 1024x768@60). Only specific viewport + If omitted, image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 2b49d654..12f58a5e 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -54,7 +54,7 @@ class BrowserRetrieveResponse(BaseModel): viewport: Optional[BrowserViewport] = None """Initial browser window size in pixels with optional refresh rate. - If omitted, image defaults apply (commonly 1024x768@60). Only specific viewport + If omitted, image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will From 177e5ea9a77f080e76c1a9e8d8c2fd29ca14de4f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 6 Dec 2025 02:01:10 +0000 Subject: [PATCH 233/448] =?UTF-8?q?feat:=20Enhance=20agent=20authenticatio?= =?UTF-8?q?n=20with=20optional=20login=20page=20URL=20and=20auth=20ch?= =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .stats.yml | 8 +- api.md | 37 ++ src/kernel/_client.py | 9 + src/kernel/resources/__init__.py | 14 + src/kernel/resources/agents/__init__.py | 33 ++ src/kernel/resources/agents/agents.py | 102 ++++ src/kernel/resources/agents/auth/__init__.py | 33 ++ src/kernel/resources/agents/auth/auth.py | 332 +++++++++++++ .../resources/agents/auth/invocations.py | 448 ++++++++++++++++++ src/kernel/types/agents/__init__.py | 11 + .../agents/agent_auth_discover_response.py | 28 ++ .../agents/agent_auth_invocation_response.py | 22 + .../types/agents/agent_auth_start_response.py | 24 + .../agents/agent_auth_submit_response.py | 34 ++ src/kernel/types/agents/auth/__init__.py | 8 + .../agents/auth/invocation_discover_params.py | 16 + .../agents/auth/invocation_exchange_params.py | 12 + .../auth/invocation_exchange_response.py | 13 + .../agents/auth/invocation_submit_params.py | 13 + src/kernel/types/agents/auth_agent.py | 21 + src/kernel/types/agents/auth_start_params.py | 33 ++ src/kernel/types/agents/discovered_field.py | 28 ++ tests/api_resources/agents/__init__.py | 1 + tests/api_resources/agents/auth/__init__.py | 1 + .../agents/auth/test_invocations.py | 421 ++++++++++++++++ tests/api_resources/agents/test_auth.py | 206 ++++++++ 26 files changed, 1904 insertions(+), 4 deletions(-) create mode 100644 src/kernel/resources/agents/__init__.py create mode 100644 src/kernel/resources/agents/agents.py create mode 100644 src/kernel/resources/agents/auth/__init__.py create mode 100644 src/kernel/resources/agents/auth/auth.py create mode 100644 src/kernel/resources/agents/auth/invocations.py create mode 100644 src/kernel/types/agents/__init__.py create mode 100644 src/kernel/types/agents/agent_auth_discover_response.py create mode 100644 src/kernel/types/agents/agent_auth_invocation_response.py create mode 100644 src/kernel/types/agents/agent_auth_start_response.py create mode 100644 src/kernel/types/agents/agent_auth_submit_response.py create mode 100644 src/kernel/types/agents/auth/__init__.py create mode 100644 src/kernel/types/agents/auth/invocation_discover_params.py create mode 100644 src/kernel/types/agents/auth/invocation_exchange_params.py create mode 100644 src/kernel/types/agents/auth/invocation_exchange_response.py create mode 100644 src/kernel/types/agents/auth/invocation_submit_params.py create mode 100644 src/kernel/types/agents/auth_agent.py create mode 100644 src/kernel/types/agents/auth_start_params.py create mode 100644 src/kernel/types/agents/discovered_field.py create mode 100644 tests/api_resources/agents/__init__.py create mode 100644 tests/api_resources/agents/auth/__init__.py create mode 100644 tests/api_resources/agents/auth/test_invocations.py create mode 100644 tests/api_resources/agents/test_auth.py diff --git a/.stats.yml b/.stats.yml index 44c807a5..0c474bba 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 74 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-3a68acd8c46e121c66be5b4c30bb4e962967840ca0f31070905baa39635fbc2d.yml -openapi_spec_hash: 9453963fbb01de3e0afb462b16cdf115 -config_hash: 6dbe88d2ba9df1ec46cedbfdb7d00000 +configured_endpoints: 80 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-8a37652fa586b8932466d16285359a89988505f850787f8257d0c4c7053da173.yml +openapi_spec_hash: 042765a113f6d08109e8146b302323ec +config_hash: 113f1e5bc3567628a5d51c70bc00969d diff --git a/api.md b/api.md index fe6b45c0..d6384dcb 100644 --- a/api.md +++ b/api.md @@ -280,3 +280,40 @@ Methods: - client.browser_pools.acquire(id_or_name, \*\*params) -> BrowserPoolAcquireResponse - client.browser_pools.flush(id_or_name) -> None - client.browser_pools.release(id_or_name, \*\*params) -> None + +# Agents + +## Auth + +Types: + +```python +from kernel.types.agents import ( + AgentAuthDiscoverResponse, + AgentAuthInvocationResponse, + AgentAuthStartResponse, + AgentAuthSubmitResponse, + AuthAgent, + DiscoveredField, +) +``` + +Methods: + +- client.agents.auth.retrieve(id) -> AuthAgent +- client.agents.auth.start(\*\*params) -> AgentAuthStartResponse + +### Invocations + +Types: + +```python +from kernel.types.agents.auth import InvocationExchangeResponse +``` + +Methods: + +- client.agents.auth.invocations.retrieve(invocation_id) -> AgentAuthInvocationResponse +- client.agents.auth.invocations.discover(invocation_id, \*\*params) -> AgentAuthDiscoverResponse +- client.agents.auth.invocations.exchange(invocation_id, \*\*params) -> InvocationExchangeResponse +- client.agents.auth.invocations.submit(invocation_id, \*\*params) -> AgentAuthSubmitResponse diff --git a/src/kernel/_client.py b/src/kernel/_client.py index 37ba4890..c941be79 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -29,6 +29,7 @@ SyncAPIClient, AsyncAPIClient, ) +from .resources.agents import agents from .resources.browsers import browsers __all__ = [ @@ -58,6 +59,7 @@ class Kernel(SyncAPIClient): proxies: proxies.ProxiesResource extensions: extensions.ExtensionsResource browser_pools: browser_pools.BrowserPoolsResource + agents: agents.AgentsResource with_raw_response: KernelWithRawResponse with_streaming_response: KernelWithStreamedResponse @@ -147,6 +149,7 @@ def __init__( self.proxies = proxies.ProxiesResource(self) self.extensions = extensions.ExtensionsResource(self) self.browser_pools = browser_pools.BrowserPoolsResource(self) + self.agents = agents.AgentsResource(self) self.with_raw_response = KernelWithRawResponse(self) self.with_streaming_response = KernelWithStreamedResponse(self) @@ -266,6 +269,7 @@ class AsyncKernel(AsyncAPIClient): proxies: proxies.AsyncProxiesResource extensions: extensions.AsyncExtensionsResource browser_pools: browser_pools.AsyncBrowserPoolsResource + agents: agents.AsyncAgentsResource with_raw_response: AsyncKernelWithRawResponse with_streaming_response: AsyncKernelWithStreamedResponse @@ -355,6 +359,7 @@ def __init__( self.proxies = proxies.AsyncProxiesResource(self) self.extensions = extensions.AsyncExtensionsResource(self) self.browser_pools = browser_pools.AsyncBrowserPoolsResource(self) + self.agents = agents.AsyncAgentsResource(self) self.with_raw_response = AsyncKernelWithRawResponse(self) self.with_streaming_response = AsyncKernelWithStreamedResponse(self) @@ -475,6 +480,7 @@ def __init__(self, client: Kernel) -> None: self.proxies = proxies.ProxiesResourceWithRawResponse(client.proxies) self.extensions = extensions.ExtensionsResourceWithRawResponse(client.extensions) self.browser_pools = browser_pools.BrowserPoolsResourceWithRawResponse(client.browser_pools) + self.agents = agents.AgentsResourceWithRawResponse(client.agents) class AsyncKernelWithRawResponse: @@ -487,6 +493,7 @@ def __init__(self, client: AsyncKernel) -> None: self.proxies = proxies.AsyncProxiesResourceWithRawResponse(client.proxies) self.extensions = extensions.AsyncExtensionsResourceWithRawResponse(client.extensions) self.browser_pools = browser_pools.AsyncBrowserPoolsResourceWithRawResponse(client.browser_pools) + self.agents = agents.AsyncAgentsResourceWithRawResponse(client.agents) class KernelWithStreamedResponse: @@ -499,6 +506,7 @@ def __init__(self, client: Kernel) -> None: self.proxies = proxies.ProxiesResourceWithStreamingResponse(client.proxies) self.extensions = extensions.ExtensionsResourceWithStreamingResponse(client.extensions) self.browser_pools = browser_pools.BrowserPoolsResourceWithStreamingResponse(client.browser_pools) + self.agents = agents.AgentsResourceWithStreamingResponse(client.agents) class AsyncKernelWithStreamedResponse: @@ -511,6 +519,7 @@ def __init__(self, client: AsyncKernel) -> None: self.proxies = proxies.AsyncProxiesResourceWithStreamingResponse(client.proxies) self.extensions = extensions.AsyncExtensionsResourceWithStreamingResponse(client.extensions) self.browser_pools = browser_pools.AsyncBrowserPoolsResourceWithStreamingResponse(client.browser_pools) + self.agents = agents.AsyncAgentsResourceWithStreamingResponse(client.agents) Client = Kernel diff --git a/src/kernel/resources/__init__.py b/src/kernel/resources/__init__.py index cf08046e..5de2a858 100644 --- a/src/kernel/resources/__init__.py +++ b/src/kernel/resources/__init__.py @@ -8,6 +8,14 @@ AppsResourceWithStreamingResponse, AsyncAppsResourceWithStreamingResponse, ) +from .agents import ( + AgentsResource, + AsyncAgentsResource, + AgentsResourceWithRawResponse, + AsyncAgentsResourceWithRawResponse, + AgentsResourceWithStreamingResponse, + AsyncAgentsResourceWithStreamingResponse, +) from .proxies import ( ProxiesResource, AsyncProxiesResource, @@ -114,4 +122,10 @@ "AsyncBrowserPoolsResourceWithRawResponse", "BrowserPoolsResourceWithStreamingResponse", "AsyncBrowserPoolsResourceWithStreamingResponse", + "AgentsResource", + "AsyncAgentsResource", + "AgentsResourceWithRawResponse", + "AsyncAgentsResourceWithRawResponse", + "AgentsResourceWithStreamingResponse", + "AsyncAgentsResourceWithStreamingResponse", ] diff --git a/src/kernel/resources/agents/__init__.py b/src/kernel/resources/agents/__init__.py new file mode 100644 index 00000000..cb159eb7 --- /dev/null +++ b/src/kernel/resources/agents/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .auth import ( + AuthResource, + AsyncAuthResource, + AuthResourceWithRawResponse, + AsyncAuthResourceWithRawResponse, + AuthResourceWithStreamingResponse, + AsyncAuthResourceWithStreamingResponse, +) +from .agents import ( + AgentsResource, + AsyncAgentsResource, + AgentsResourceWithRawResponse, + AsyncAgentsResourceWithRawResponse, + AgentsResourceWithStreamingResponse, + AsyncAgentsResourceWithStreamingResponse, +) + +__all__ = [ + "AuthResource", + "AsyncAuthResource", + "AuthResourceWithRawResponse", + "AsyncAuthResourceWithRawResponse", + "AuthResourceWithStreamingResponse", + "AsyncAuthResourceWithStreamingResponse", + "AgentsResource", + "AsyncAgentsResource", + "AgentsResourceWithRawResponse", + "AsyncAgentsResourceWithRawResponse", + "AgentsResourceWithStreamingResponse", + "AsyncAgentsResourceWithStreamingResponse", +] diff --git a/src/kernel/resources/agents/agents.py b/src/kernel/resources/agents/agents.py new file mode 100644 index 00000000..b7bb580c --- /dev/null +++ b/src/kernel/resources/agents/agents.py @@ -0,0 +1,102 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from ..._compat import cached_property +from .auth.auth import ( + AuthResource, + AsyncAuthResource, + AuthResourceWithRawResponse, + AsyncAuthResourceWithRawResponse, + AuthResourceWithStreamingResponse, + AsyncAuthResourceWithStreamingResponse, +) +from ..._resource import SyncAPIResource, AsyncAPIResource + +__all__ = ["AgentsResource", "AsyncAgentsResource"] + + +class AgentsResource(SyncAPIResource): + @cached_property + def auth(self) -> AuthResource: + return AuthResource(self._client) + + @cached_property + def with_raw_response(self) -> AgentsResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AgentsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AgentsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return AgentsResourceWithStreamingResponse(self) + + +class AsyncAgentsResource(AsyncAPIResource): + @cached_property + def auth(self) -> AsyncAuthResource: + return AsyncAuthResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncAgentsResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncAgentsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAgentsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return AsyncAgentsResourceWithStreamingResponse(self) + + +class AgentsResourceWithRawResponse: + def __init__(self, agents: AgentsResource) -> None: + self._agents = agents + + @cached_property + def auth(self) -> AuthResourceWithRawResponse: + return AuthResourceWithRawResponse(self._agents.auth) + + +class AsyncAgentsResourceWithRawResponse: + def __init__(self, agents: AsyncAgentsResource) -> None: + self._agents = agents + + @cached_property + def auth(self) -> AsyncAuthResourceWithRawResponse: + return AsyncAuthResourceWithRawResponse(self._agents.auth) + + +class AgentsResourceWithStreamingResponse: + def __init__(self, agents: AgentsResource) -> None: + self._agents = agents + + @cached_property + def auth(self) -> AuthResourceWithStreamingResponse: + return AuthResourceWithStreamingResponse(self._agents.auth) + + +class AsyncAgentsResourceWithStreamingResponse: + def __init__(self, agents: AsyncAgentsResource) -> None: + self._agents = agents + + @cached_property + def auth(self) -> AsyncAuthResourceWithStreamingResponse: + return AsyncAuthResourceWithStreamingResponse(self._agents.auth) diff --git a/src/kernel/resources/agents/auth/__init__.py b/src/kernel/resources/agents/auth/__init__.py new file mode 100644 index 00000000..61305493 --- /dev/null +++ b/src/kernel/resources/agents/auth/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .auth import ( + AuthResource, + AsyncAuthResource, + AuthResourceWithRawResponse, + AsyncAuthResourceWithRawResponse, + AuthResourceWithStreamingResponse, + AsyncAuthResourceWithStreamingResponse, +) +from .invocations import ( + InvocationsResource, + AsyncInvocationsResource, + InvocationsResourceWithRawResponse, + AsyncInvocationsResourceWithRawResponse, + InvocationsResourceWithStreamingResponse, + AsyncInvocationsResourceWithStreamingResponse, +) + +__all__ = [ + "InvocationsResource", + "AsyncInvocationsResource", + "InvocationsResourceWithRawResponse", + "AsyncInvocationsResourceWithRawResponse", + "InvocationsResourceWithStreamingResponse", + "AsyncInvocationsResourceWithStreamingResponse", + "AuthResource", + "AsyncAuthResource", + "AuthResourceWithRawResponse", + "AsyncAuthResourceWithRawResponse", + "AuthResourceWithStreamingResponse", + "AsyncAuthResourceWithStreamingResponse", +] diff --git a/src/kernel/resources/agents/auth/auth.py b/src/kernel/resources/agents/auth/auth.py new file mode 100644 index 00000000..daa82210 --- /dev/null +++ b/src/kernel/resources/agents/auth/auth.py @@ -0,0 +1,332 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ...._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from ...._utils import maybe_transform, async_maybe_transform +from ...._compat import cached_property +from .invocations import ( + InvocationsResource, + AsyncInvocationsResource, + InvocationsResourceWithRawResponse, + AsyncInvocationsResourceWithRawResponse, + InvocationsResourceWithStreamingResponse, + AsyncInvocationsResourceWithStreamingResponse, +) +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.agents import auth_start_params +from ....types.agents.auth_agent import AuthAgent +from ....types.agents.agent_auth_start_response import AgentAuthStartResponse + +__all__ = ["AuthResource", "AsyncAuthResource"] + + +class AuthResource(SyncAPIResource): + @cached_property + def invocations(self) -> InvocationsResource: + return InvocationsResource(self._client) + + @cached_property + def with_raw_response(self) -> AuthResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AuthResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AuthResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return AuthResourceWithStreamingResponse(self) + + def retrieve( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AuthAgent: + """Retrieve an auth agent by its ID. + + Returns the current authentication status of + the managed profile. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/agents/auth/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AuthAgent, + ) + + def start( + self, + *, + profile_name: str, + target_domain: str, + app_logo_url: str | Omit = omit, + login_url: str | Omit = omit, + proxy: auth_start_params.Proxy | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AgentAuthStartResponse: + """Creates a browser session and returns a handoff code for the hosted flow. + + Uses + standard API key or JWT authentication (not the JWT returned by the exchange + endpoint). + + Args: + profile_name: Name of the profile to use for this flow + + target_domain: Target domain for authentication + + app_logo_url: Optional logo URL for the application + + login_url: Optional login page URL. If provided, will be stored on the agent and used to + skip Phase 1 discovery in future invocations. + + proxy: Optional proxy configuration + + 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( + "/agents/auth/start", + body=maybe_transform( + { + "profile_name": profile_name, + "target_domain": target_domain, + "app_logo_url": app_logo_url, + "login_url": login_url, + "proxy": proxy, + }, + auth_start_params.AuthStartParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AgentAuthStartResponse, + ) + + +class AsyncAuthResource(AsyncAPIResource): + @cached_property + def invocations(self) -> AsyncInvocationsResource: + return AsyncInvocationsResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncAuthResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncAuthResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAuthResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return AsyncAuthResourceWithStreamingResponse(self) + + async def retrieve( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AuthAgent: + """Retrieve an auth agent by its ID. + + Returns the current authentication status of + the managed profile. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/agents/auth/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AuthAgent, + ) + + async def start( + self, + *, + profile_name: str, + target_domain: str, + app_logo_url: str | Omit = omit, + login_url: str | Omit = omit, + proxy: auth_start_params.Proxy | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AgentAuthStartResponse: + """Creates a browser session and returns a handoff code for the hosted flow. + + Uses + standard API key or JWT authentication (not the JWT returned by the exchange + endpoint). + + Args: + profile_name: Name of the profile to use for this flow + + target_domain: Target domain for authentication + + app_logo_url: Optional logo URL for the application + + login_url: Optional login page URL. If provided, will be stored on the agent and used to + skip Phase 1 discovery in future invocations. + + proxy: Optional proxy configuration + + 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( + "/agents/auth/start", + body=await async_maybe_transform( + { + "profile_name": profile_name, + "target_domain": target_domain, + "app_logo_url": app_logo_url, + "login_url": login_url, + "proxy": proxy, + }, + auth_start_params.AuthStartParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AgentAuthStartResponse, + ) + + +class AuthResourceWithRawResponse: + def __init__(self, auth: AuthResource) -> None: + self._auth = auth + + self.retrieve = to_raw_response_wrapper( + auth.retrieve, + ) + self.start = to_raw_response_wrapper( + auth.start, + ) + + @cached_property + def invocations(self) -> InvocationsResourceWithRawResponse: + return InvocationsResourceWithRawResponse(self._auth.invocations) + + +class AsyncAuthResourceWithRawResponse: + def __init__(self, auth: AsyncAuthResource) -> None: + self._auth = auth + + self.retrieve = async_to_raw_response_wrapper( + auth.retrieve, + ) + self.start = async_to_raw_response_wrapper( + auth.start, + ) + + @cached_property + def invocations(self) -> AsyncInvocationsResourceWithRawResponse: + return AsyncInvocationsResourceWithRawResponse(self._auth.invocations) + + +class AuthResourceWithStreamingResponse: + def __init__(self, auth: AuthResource) -> None: + self._auth = auth + + self.retrieve = to_streamed_response_wrapper( + auth.retrieve, + ) + self.start = to_streamed_response_wrapper( + auth.start, + ) + + @cached_property + def invocations(self) -> InvocationsResourceWithStreamingResponse: + return InvocationsResourceWithStreamingResponse(self._auth.invocations) + + +class AsyncAuthResourceWithStreamingResponse: + def __init__(self, auth: AsyncAuthResource) -> None: + self._auth = auth + + self.retrieve = async_to_streamed_response_wrapper( + auth.retrieve, + ) + self.start = async_to_streamed_response_wrapper( + auth.start, + ) + + @cached_property + def invocations(self) -> AsyncInvocationsResourceWithStreamingResponse: + return AsyncInvocationsResourceWithStreamingResponse(self._auth.invocations) diff --git a/src/kernel/resources/agents/auth/invocations.py b/src/kernel/resources/agents/auth/invocations.py new file mode 100644 index 00000000..15729ed8 --- /dev/null +++ b/src/kernel/resources/agents/auth/invocations.py @@ -0,0 +1,448 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict + +import httpx + +from ...._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from ...._utils import maybe_transform, async_maybe_transform +from ...._compat import cached_property +from ...._resource import SyncAPIResource, AsyncAPIResource +from ...._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ...._base_client import make_request_options +from ....types.agents.auth import invocation_submit_params, invocation_discover_params, invocation_exchange_params +from ....types.agents.agent_auth_submit_response import AgentAuthSubmitResponse +from ....types.agents.agent_auth_discover_response import AgentAuthDiscoverResponse +from ....types.agents.agent_auth_invocation_response import AgentAuthInvocationResponse +from ....types.agents.auth.invocation_exchange_response import InvocationExchangeResponse + +__all__ = ["InvocationsResource", "AsyncInvocationsResource"] + + +class InvocationsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> InvocationsResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return InvocationsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> InvocationsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return InvocationsResourceWithStreamingResponse(self) + + def retrieve( + self, + invocation_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, + ) -> AgentAuthInvocationResponse: + """Returns invocation details including app_name and target_domain. + + Uses the JWT + returned by the exchange endpoint, or standard API key or JWT authentication. + + 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 invocation_id: + raise ValueError(f"Expected a non-empty value for `invocation_id` but received {invocation_id!r}") + return self._get( + f"/agents/auth/invocations/{invocation_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AgentAuthInvocationResponse, + ) + + def discover( + self, + invocation_id: str, + *, + login_url: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AgentAuthDiscoverResponse: + """ + Inspects the target site to detect logged-in state or discover required fields. + Returns 200 with success: true when fields are found, or 4xx/5xx for failures. + Requires the JWT returned by the exchange endpoint. + + Args: + login_url: Optional login page URL. If provided, will override the stored login URL for + this discovery invocation and skip Phase 1 discovery. + + 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 invocation_id: + raise ValueError(f"Expected a non-empty value for `invocation_id` but received {invocation_id!r}") + return self._post( + f"/agents/auth/invocations/{invocation_id}/discover", + body=maybe_transform({"login_url": login_url}, invocation_discover_params.InvocationDiscoverParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AgentAuthDiscoverResponse, + ) + + def exchange( + self, + invocation_id: str, + *, + code: 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, + ) -> InvocationExchangeResponse: + """Validates the handoff code and returns a JWT token for subsequent requests. + + No + authentication required (the handoff code serves as the credential). + + Args: + code: Handoff code from start endpoint + + 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 invocation_id: + raise ValueError(f"Expected a non-empty value for `invocation_id` but received {invocation_id!r}") + return self._post( + f"/agents/auth/invocations/{invocation_id}/exchange", + body=maybe_transform({"code": code}, invocation_exchange_params.InvocationExchangeParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=InvocationExchangeResponse, + ) + + def submit( + self, + invocation_id: str, + *, + field_values: Dict[str, 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, + ) -> AgentAuthSubmitResponse: + """ + Submits field values for the discovered login form and may return additional + auth fields or success. Requires the JWT returned by the exchange endpoint. + + Args: + field_values: Values for the discovered login fields + + 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 invocation_id: + raise ValueError(f"Expected a non-empty value for `invocation_id` but received {invocation_id!r}") + return self._post( + f"/agents/auth/invocations/{invocation_id}/submit", + body=maybe_transform({"field_values": field_values}, invocation_submit_params.InvocationSubmitParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AgentAuthSubmitResponse, + ) + + +class AsyncInvocationsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncInvocationsResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncInvocationsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncInvocationsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return AsyncInvocationsResourceWithStreamingResponse(self) + + async def retrieve( + self, + invocation_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, + ) -> AgentAuthInvocationResponse: + """Returns invocation details including app_name and target_domain. + + Uses the JWT + returned by the exchange endpoint, or standard API key or JWT authentication. + + 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 invocation_id: + raise ValueError(f"Expected a non-empty value for `invocation_id` but received {invocation_id!r}") + return await self._get( + f"/agents/auth/invocations/{invocation_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AgentAuthInvocationResponse, + ) + + async def discover( + self, + invocation_id: str, + *, + login_url: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AgentAuthDiscoverResponse: + """ + Inspects the target site to detect logged-in state or discover required fields. + Returns 200 with success: true when fields are found, or 4xx/5xx for failures. + Requires the JWT returned by the exchange endpoint. + + Args: + login_url: Optional login page URL. If provided, will override the stored login URL for + this discovery invocation and skip Phase 1 discovery. + + 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 invocation_id: + raise ValueError(f"Expected a non-empty value for `invocation_id` but received {invocation_id!r}") + return await self._post( + f"/agents/auth/invocations/{invocation_id}/discover", + body=await async_maybe_transform( + {"login_url": login_url}, invocation_discover_params.InvocationDiscoverParams + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AgentAuthDiscoverResponse, + ) + + async def exchange( + self, + invocation_id: str, + *, + code: 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, + ) -> InvocationExchangeResponse: + """Validates the handoff code and returns a JWT token for subsequent requests. + + No + authentication required (the handoff code serves as the credential). + + Args: + code: Handoff code from start endpoint + + 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 invocation_id: + raise ValueError(f"Expected a non-empty value for `invocation_id` but received {invocation_id!r}") + return await self._post( + f"/agents/auth/invocations/{invocation_id}/exchange", + body=await async_maybe_transform({"code": code}, invocation_exchange_params.InvocationExchangeParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=InvocationExchangeResponse, + ) + + async def submit( + self, + invocation_id: str, + *, + field_values: Dict[str, 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, + ) -> AgentAuthSubmitResponse: + """ + Submits field values for the discovered login form and may return additional + auth fields or success. Requires the JWT returned by the exchange endpoint. + + Args: + field_values: Values for the discovered login fields + + 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 invocation_id: + raise ValueError(f"Expected a non-empty value for `invocation_id` but received {invocation_id!r}") + return await self._post( + f"/agents/auth/invocations/{invocation_id}/submit", + body=await async_maybe_transform( + {"field_values": field_values}, invocation_submit_params.InvocationSubmitParams + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AgentAuthSubmitResponse, + ) + + +class InvocationsResourceWithRawResponse: + def __init__(self, invocations: InvocationsResource) -> None: + self._invocations = invocations + + self.retrieve = to_raw_response_wrapper( + invocations.retrieve, + ) + self.discover = to_raw_response_wrapper( + invocations.discover, + ) + self.exchange = to_raw_response_wrapper( + invocations.exchange, + ) + self.submit = to_raw_response_wrapper( + invocations.submit, + ) + + +class AsyncInvocationsResourceWithRawResponse: + def __init__(self, invocations: AsyncInvocationsResource) -> None: + self._invocations = invocations + + self.retrieve = async_to_raw_response_wrapper( + invocations.retrieve, + ) + self.discover = async_to_raw_response_wrapper( + invocations.discover, + ) + self.exchange = async_to_raw_response_wrapper( + invocations.exchange, + ) + self.submit = async_to_raw_response_wrapper( + invocations.submit, + ) + + +class InvocationsResourceWithStreamingResponse: + def __init__(self, invocations: InvocationsResource) -> None: + self._invocations = invocations + + self.retrieve = to_streamed_response_wrapper( + invocations.retrieve, + ) + self.discover = to_streamed_response_wrapper( + invocations.discover, + ) + self.exchange = to_streamed_response_wrapper( + invocations.exchange, + ) + self.submit = to_streamed_response_wrapper( + invocations.submit, + ) + + +class AsyncInvocationsResourceWithStreamingResponse: + def __init__(self, invocations: AsyncInvocationsResource) -> None: + self._invocations = invocations + + self.retrieve = async_to_streamed_response_wrapper( + invocations.retrieve, + ) + self.discover = async_to_streamed_response_wrapper( + invocations.discover, + ) + self.exchange = async_to_streamed_response_wrapper( + invocations.exchange, + ) + self.submit = async_to_streamed_response_wrapper( + invocations.submit, + ) diff --git a/src/kernel/types/agents/__init__.py b/src/kernel/types/agents/__init__.py new file mode 100644 index 00000000..1fdcc09b --- /dev/null +++ b/src/kernel/types/agents/__init__.py @@ -0,0 +1,11 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .auth_agent import AuthAgent as AuthAgent +from .discovered_field import DiscoveredField as DiscoveredField +from .auth_start_params import AuthStartParams as AuthStartParams +from .agent_auth_start_response import AgentAuthStartResponse as AgentAuthStartResponse +from .agent_auth_submit_response import AgentAuthSubmitResponse as AgentAuthSubmitResponse +from .agent_auth_discover_response import AgentAuthDiscoverResponse as AgentAuthDiscoverResponse +from .agent_auth_invocation_response import AgentAuthInvocationResponse as AgentAuthInvocationResponse diff --git a/src/kernel/types/agents/agent_auth_discover_response.py b/src/kernel/types/agents/agent_auth_discover_response.py new file mode 100644 index 00000000..000bdec2 --- /dev/null +++ b/src/kernel/types/agents/agent_auth_discover_response.py @@ -0,0 +1,28 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from ..._models import BaseModel +from .discovered_field import DiscoveredField + +__all__ = ["AgentAuthDiscoverResponse"] + + +class AgentAuthDiscoverResponse(BaseModel): + success: bool + """Whether discovery succeeded""" + + error_message: Optional[str] = None + """Error message if discovery failed""" + + fields: Optional[List[DiscoveredField]] = None + """Discovered form fields (present when success is true)""" + + logged_in: Optional[bool] = None + """Whether user is already logged in""" + + login_url: Optional[str] = None + """URL of the discovered login page""" + + page_title: Optional[str] = None + """Title of the login page""" diff --git a/src/kernel/types/agents/agent_auth_invocation_response.py b/src/kernel/types/agents/agent_auth_invocation_response.py new file mode 100644 index 00000000..82b5f80e --- /dev/null +++ b/src/kernel/types/agents/agent_auth_invocation_response.py @@ -0,0 +1,22 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from datetime import datetime +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["AgentAuthInvocationResponse"] + + +class AgentAuthInvocationResponse(BaseModel): + app_name: str + """App name (org name at time of invocation creation)""" + + expires_at: datetime + """When the handoff code expires""" + + status: Literal["IN_PROGRESS", "SUCCESS", "EXPIRED", "CANCELED"] + """Invocation status""" + + target_domain: str + """Target domain for authentication""" diff --git a/src/kernel/types/agents/agent_auth_start_response.py b/src/kernel/types/agents/agent_auth_start_response.py new file mode 100644 index 00000000..3287ba0b --- /dev/null +++ b/src/kernel/types/agents/agent_auth_start_response.py @@ -0,0 +1,24 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from datetime import datetime + +from ..._models import BaseModel + +__all__ = ["AgentAuthStartResponse"] + + +class AgentAuthStartResponse(BaseModel): + auth_agent_id: str + """Unique identifier for the auth agent managing this domain/profile""" + + expires_at: datetime + """When the handoff code expires""" + + handoff_code: str + """One-time code for handoff""" + + hosted_url: str + """URL to redirect user to""" + + invocation_id: str + """Unique identifier for the invocation""" diff --git a/src/kernel/types/agents/agent_auth_submit_response.py b/src/kernel/types/agents/agent_auth_submit_response.py new file mode 100644 index 00000000..c57002fb --- /dev/null +++ b/src/kernel/types/agents/agent_auth_submit_response.py @@ -0,0 +1,34 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from ..._models import BaseModel +from .discovered_field import DiscoveredField + +__all__ = ["AgentAuthSubmitResponse"] + + +class AgentAuthSubmitResponse(BaseModel): + success: bool + """Whether submission succeeded""" + + additional_fields: Optional[List[DiscoveredField]] = None + """ + Additional fields needed (e.g., OTP) - present when needs_additional_auth is + true + """ + + app_name: Optional[str] = None + """App name (only present when logged_in is true)""" + + error_message: Optional[str] = None + """Error message if submission failed""" + + logged_in: Optional[bool] = None + """Whether user is now logged in""" + + needs_additional_auth: Optional[bool] = None + """Whether additional authentication fields are needed""" + + target_domain: Optional[str] = None + """Target domain (only present when logged_in is true)""" diff --git a/src/kernel/types/agents/auth/__init__.py b/src/kernel/types/agents/auth/__init__.py new file mode 100644 index 00000000..bfbd2801 --- /dev/null +++ b/src/kernel/types/agents/auth/__init__.py @@ -0,0 +1,8 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .invocation_submit_params import InvocationSubmitParams as InvocationSubmitParams +from .invocation_discover_params import InvocationDiscoverParams as InvocationDiscoverParams +from .invocation_exchange_params import InvocationExchangeParams as InvocationExchangeParams +from .invocation_exchange_response import InvocationExchangeResponse as InvocationExchangeResponse diff --git a/src/kernel/types/agents/auth/invocation_discover_params.py b/src/kernel/types/agents/auth/invocation_discover_params.py new file mode 100644 index 00000000..aa03f0cd --- /dev/null +++ b/src/kernel/types/agents/auth/invocation_discover_params.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["InvocationDiscoverParams"] + + +class InvocationDiscoverParams(TypedDict, total=False): + login_url: str + """Optional login page URL. + + If provided, will override the stored login URL for this discovery invocation + and skip Phase 1 discovery. + """ diff --git a/src/kernel/types/agents/auth/invocation_exchange_params.py b/src/kernel/types/agents/auth/invocation_exchange_params.py new file mode 100644 index 00000000..71e4d184 --- /dev/null +++ b/src/kernel/types/agents/auth/invocation_exchange_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["InvocationExchangeParams"] + + +class InvocationExchangeParams(TypedDict, total=False): + code: Required[str] + """Handoff code from start endpoint""" diff --git a/src/kernel/types/agents/auth/invocation_exchange_response.py b/src/kernel/types/agents/auth/invocation_exchange_response.py new file mode 100644 index 00000000..91b74ceb --- /dev/null +++ b/src/kernel/types/agents/auth/invocation_exchange_response.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from ...._models import BaseModel + +__all__ = ["InvocationExchangeResponse"] + + +class InvocationExchangeResponse(BaseModel): + invocation_id: str + """Invocation ID""" + + jwt: str + """JWT token with invocation_id claim (30 minute TTL)""" diff --git a/src/kernel/types/agents/auth/invocation_submit_params.py b/src/kernel/types/agents/auth/invocation_submit_params.py new file mode 100644 index 00000000..be92e7de --- /dev/null +++ b/src/kernel/types/agents/auth/invocation_submit_params.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict +from typing_extensions import Required, TypedDict + +__all__ = ["InvocationSubmitParams"] + + +class InvocationSubmitParams(TypedDict, total=False): + field_values: Required[Dict[str, str]] + """Values for the discovered login fields""" diff --git a/src/kernel/types/agents/auth_agent.py b/src/kernel/types/agents/auth_agent.py new file mode 100644 index 00000000..8671f97c --- /dev/null +++ b/src/kernel/types/agents/auth_agent.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["AuthAgent"] + + +class AuthAgent(BaseModel): + id: str + """Unique identifier for the auth agent""" + + domain: str + """Target domain for authentication""" + + profile_name: str + """Name of the profile associated with this auth agent""" + + status: Literal["AUTHENTICATED", "NEEDS_AUTH"] + """Current authentication status of the managed profile""" diff --git a/src/kernel/types/agents/auth_start_params.py b/src/kernel/types/agents/auth_start_params.py new file mode 100644 index 00000000..9c9fb35a --- /dev/null +++ b/src/kernel/types/agents/auth_start_params.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["AuthStartParams", "Proxy"] + + +class AuthStartParams(TypedDict, total=False): + profile_name: Required[str] + """Name of the profile to use for this flow""" + + target_domain: Required[str] + """Target domain for authentication""" + + app_logo_url: str + """Optional logo URL for the application""" + + login_url: str + """Optional login page URL. + + If provided, will be stored on the agent and used to skip Phase 1 discovery in + future invocations. + """ + + proxy: Proxy + """Optional proxy configuration""" + + +class Proxy(TypedDict, total=False): + proxy_id: str + """ID of the proxy to use""" diff --git a/src/kernel/types/agents/discovered_field.py b/src/kernel/types/agents/discovered_field.py new file mode 100644 index 00000000..d1b9dc9d --- /dev/null +++ b/src/kernel/types/agents/discovered_field.py @@ -0,0 +1,28 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["DiscoveredField"] + + +class DiscoveredField(BaseModel): + label: str + """Field label""" + + name: str + """Field name""" + + selector: str + """CSS selector for the field""" + + type: Literal["text", "email", "password", "tel", "number", "url", "code"] + """Field type""" + + placeholder: Optional[str] = None + """Field placeholder""" + + required: Optional[bool] = None + """Whether field is required""" diff --git a/tests/api_resources/agents/__init__.py b/tests/api_resources/agents/__init__.py new file mode 100644 index 00000000..fd8019a9 --- /dev/null +++ b/tests/api_resources/agents/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/agents/auth/__init__.py b/tests/api_resources/agents/auth/__init__.py new file mode 100644 index 00000000..fd8019a9 --- /dev/null +++ b/tests/api_resources/agents/auth/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/agents/auth/test_invocations.py b/tests/api_resources/agents/auth/test_invocations.py new file mode 100644 index 00000000..e9c3d8f7 --- /dev/null +++ b/tests/api_resources/agents/auth/test_invocations.py @@ -0,0 +1,421 @@ +# 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 kernel import Kernel, AsyncKernel +from tests.utils import assert_matches_type +from kernel.types.agents import AgentAuthSubmitResponse, AgentAuthDiscoverResponse, AgentAuthInvocationResponse +from kernel.types.agents.auth import ( + InvocationExchangeResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestInvocations: + 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: Kernel) -> None: + invocation = client.agents.auth.invocations.retrieve( + "invocation_id", + ) + assert_matches_type(AgentAuthInvocationResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: Kernel) -> None: + response = client.agents.auth.invocations.with_raw_response.retrieve( + "invocation_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + invocation = response.parse() + assert_matches_type(AgentAuthInvocationResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: Kernel) -> None: + with client.agents.auth.invocations.with_streaming_response.retrieve( + "invocation_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + invocation = response.parse() + assert_matches_type(AgentAuthInvocationResponse, invocation, 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: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): + client.agents.auth.invocations.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_discover(self, client: Kernel) -> None: + invocation = client.agents.auth.invocations.discover( + invocation_id="invocation_id", + ) + assert_matches_type(AgentAuthDiscoverResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_discover_with_all_params(self, client: Kernel) -> None: + invocation = client.agents.auth.invocations.discover( + invocation_id="invocation_id", + login_url="https://doordash.com/account/login", + ) + assert_matches_type(AgentAuthDiscoverResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_discover(self, client: Kernel) -> None: + response = client.agents.auth.invocations.with_raw_response.discover( + invocation_id="invocation_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + invocation = response.parse() + assert_matches_type(AgentAuthDiscoverResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_discover(self, client: Kernel) -> None: + with client.agents.auth.invocations.with_streaming_response.discover( + invocation_id="invocation_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + invocation = response.parse() + assert_matches_type(AgentAuthDiscoverResponse, invocation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_discover(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): + client.agents.auth.invocations.with_raw_response.discover( + invocation_id="", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_exchange(self, client: Kernel) -> None: + invocation = client.agents.auth.invocations.exchange( + invocation_id="invocation_id", + code="abc123xyz", + ) + assert_matches_type(InvocationExchangeResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_exchange(self, client: Kernel) -> None: + response = client.agents.auth.invocations.with_raw_response.exchange( + invocation_id="invocation_id", + code="abc123xyz", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + invocation = response.parse() + assert_matches_type(InvocationExchangeResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_exchange(self, client: Kernel) -> None: + with client.agents.auth.invocations.with_streaming_response.exchange( + invocation_id="invocation_id", + code="abc123xyz", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + invocation = response.parse() + assert_matches_type(InvocationExchangeResponse, invocation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_exchange(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): + client.agents.auth.invocations.with_raw_response.exchange( + invocation_id="", + code="abc123xyz", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_submit(self, client: Kernel) -> None: + invocation = client.agents.auth.invocations.submit( + invocation_id="invocation_id", + field_values={ + "email": "user@example.com", + "password": "********", + }, + ) + assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_submit(self, client: Kernel) -> None: + response = client.agents.auth.invocations.with_raw_response.submit( + invocation_id="invocation_id", + field_values={ + "email": "user@example.com", + "password": "********", + }, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + invocation = response.parse() + assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_submit(self, client: Kernel) -> None: + with client.agents.auth.invocations.with_streaming_response.submit( + invocation_id="invocation_id", + field_values={ + "email": "user@example.com", + "password": "********", + }, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + invocation = response.parse() + assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_submit(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): + client.agents.auth.invocations.with_raw_response.submit( + invocation_id="", + field_values={ + "email": "user@example.com", + "password": "********", + }, + ) + + +class TestAsyncInvocations: + 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: AsyncKernel) -> None: + invocation = await async_client.agents.auth.invocations.retrieve( + "invocation_id", + ) + assert_matches_type(AgentAuthInvocationResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: + response = await async_client.agents.auth.invocations.with_raw_response.retrieve( + "invocation_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + invocation = await response.parse() + assert_matches_type(AgentAuthInvocationResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: + async with async_client.agents.auth.invocations.with_streaming_response.retrieve( + "invocation_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + invocation = await response.parse() + assert_matches_type(AgentAuthInvocationResponse, invocation, 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: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): + await async_client.agents.auth.invocations.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_discover(self, async_client: AsyncKernel) -> None: + invocation = await async_client.agents.auth.invocations.discover( + invocation_id="invocation_id", + ) + assert_matches_type(AgentAuthDiscoverResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_discover_with_all_params(self, async_client: AsyncKernel) -> None: + invocation = await async_client.agents.auth.invocations.discover( + invocation_id="invocation_id", + login_url="https://doordash.com/account/login", + ) + assert_matches_type(AgentAuthDiscoverResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_discover(self, async_client: AsyncKernel) -> None: + response = await async_client.agents.auth.invocations.with_raw_response.discover( + invocation_id="invocation_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + invocation = await response.parse() + assert_matches_type(AgentAuthDiscoverResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_discover(self, async_client: AsyncKernel) -> None: + async with async_client.agents.auth.invocations.with_streaming_response.discover( + invocation_id="invocation_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + invocation = await response.parse() + assert_matches_type(AgentAuthDiscoverResponse, invocation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_discover(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): + await async_client.agents.auth.invocations.with_raw_response.discover( + invocation_id="", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_exchange(self, async_client: AsyncKernel) -> None: + invocation = await async_client.agents.auth.invocations.exchange( + invocation_id="invocation_id", + code="abc123xyz", + ) + assert_matches_type(InvocationExchangeResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_exchange(self, async_client: AsyncKernel) -> None: + response = await async_client.agents.auth.invocations.with_raw_response.exchange( + invocation_id="invocation_id", + code="abc123xyz", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + invocation = await response.parse() + assert_matches_type(InvocationExchangeResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_exchange(self, async_client: AsyncKernel) -> None: + async with async_client.agents.auth.invocations.with_streaming_response.exchange( + invocation_id="invocation_id", + code="abc123xyz", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + invocation = await response.parse() + assert_matches_type(InvocationExchangeResponse, invocation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_exchange(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): + await async_client.agents.auth.invocations.with_raw_response.exchange( + invocation_id="", + code="abc123xyz", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_submit(self, async_client: AsyncKernel) -> None: + invocation = await async_client.agents.auth.invocations.submit( + invocation_id="invocation_id", + field_values={ + "email": "user@example.com", + "password": "********", + }, + ) + assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_submit(self, async_client: AsyncKernel) -> None: + response = await async_client.agents.auth.invocations.with_raw_response.submit( + invocation_id="invocation_id", + field_values={ + "email": "user@example.com", + "password": "********", + }, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + invocation = await response.parse() + assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_submit(self, async_client: AsyncKernel) -> None: + async with async_client.agents.auth.invocations.with_streaming_response.submit( + invocation_id="invocation_id", + field_values={ + "email": "user@example.com", + "password": "********", + }, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + invocation = await response.parse() + assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_submit(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): + await async_client.agents.auth.invocations.with_raw_response.submit( + invocation_id="", + field_values={ + "email": "user@example.com", + "password": "********", + }, + ) diff --git a/tests/api_resources/agents/test_auth.py b/tests/api_resources/agents/test_auth.py new file mode 100644 index 00000000..0dc190d8 --- /dev/null +++ b/tests/api_resources/agents/test_auth.py @@ -0,0 +1,206 @@ +# 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 kernel import Kernel, AsyncKernel +from tests.utils import assert_matches_type +from kernel.types.agents import AuthAgent, AgentAuthStartResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestAuth: + 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: Kernel) -> None: + auth = client.agents.auth.retrieve( + "id", + ) + assert_matches_type(AuthAgent, auth, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: Kernel) -> None: + response = client.agents.auth.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + auth = response.parse() + assert_matches_type(AuthAgent, auth, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: Kernel) -> None: + with client.agents.auth.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + auth = response.parse() + assert_matches_type(AuthAgent, auth, 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: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.agents.auth.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_start(self, client: Kernel) -> None: + auth = client.agents.auth.start( + profile_name="auth-abc123", + target_domain="doordash.com", + ) + assert_matches_type(AgentAuthStartResponse, auth, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_start_with_all_params(self, client: Kernel) -> None: + auth = client.agents.auth.start( + profile_name="auth-abc123", + target_domain="doordash.com", + app_logo_url="https://example.com/logo.png", + login_url="https://doordash.com/account/login", + proxy={"proxy_id": "proxy_id"}, + ) + assert_matches_type(AgentAuthStartResponse, auth, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_start(self, client: Kernel) -> None: + response = client.agents.auth.with_raw_response.start( + profile_name="auth-abc123", + target_domain="doordash.com", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + auth = response.parse() + assert_matches_type(AgentAuthStartResponse, auth, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_start(self, client: Kernel) -> None: + with client.agents.auth.with_streaming_response.start( + profile_name="auth-abc123", + target_domain="doordash.com", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + auth = response.parse() + assert_matches_type(AgentAuthStartResponse, auth, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncAuth: + 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: AsyncKernel) -> None: + auth = await async_client.agents.auth.retrieve( + "id", + ) + assert_matches_type(AuthAgent, auth, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: + response = await async_client.agents.auth.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + auth = await response.parse() + assert_matches_type(AuthAgent, auth, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: + async with async_client.agents.auth.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + auth = await response.parse() + assert_matches_type(AuthAgent, auth, 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: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.agents.auth.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_start(self, async_client: AsyncKernel) -> None: + auth = await async_client.agents.auth.start( + profile_name="auth-abc123", + target_domain="doordash.com", + ) + assert_matches_type(AgentAuthStartResponse, auth, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_start_with_all_params(self, async_client: AsyncKernel) -> None: + auth = await async_client.agents.auth.start( + profile_name="auth-abc123", + target_domain="doordash.com", + app_logo_url="https://example.com/logo.png", + login_url="https://doordash.com/account/login", + proxy={"proxy_id": "proxy_id"}, + ) + assert_matches_type(AgentAuthStartResponse, auth, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_start(self, async_client: AsyncKernel) -> None: + response = await async_client.agents.auth.with_raw_response.start( + profile_name="auth-abc123", + target_domain="doordash.com", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + auth = await response.parse() + assert_matches_type(AgentAuthStartResponse, auth, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_start(self, async_client: AsyncKernel) -> None: + async with async_client.agents.auth.with_streaming_response.start( + profile_name="auth-abc123", + target_domain="doordash.com", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + auth = await response.parse() + assert_matches_type(AgentAuthStartResponse, auth, path=["response"]) + + assert cast(Any, response.is_closed) is True From ec7558dfb29ce240b4ecb37713b69ce1dcd71a7f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 06:55:42 +0000 Subject: [PATCH 234/448] =?UTF-8?q?feat:=20enhance=20agent=20authenticatio?= =?UTF-8?q?n=20API=20with=20new=20endpoints=20and=20request=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .stats.yml | 8 +- api.md | 8 +- src/kernel/resources/agents/auth/auth.py | 268 +++++++++++++----- .../resources/agents/auth/invocations.py | 96 ++++++- src/kernel/types/agents/__init__.py | 7 +- src/kernel/types/agents/auth/__init__.py | 1 + .../agents/auth/invocation_create_params.py | 12 + ... auth_agent_invocation_create_response.py} | 7 +- ..._start_params.py => auth_create_params.py} | 13 +- src/kernel/types/agents/auth_list_params.py | 21 ++ .../agents/auth/test_invocations.py | 75 ++++- tests/api_resources/agents/test_auth.py | 183 ++++++++---- 12 files changed, 546 insertions(+), 153 deletions(-) create mode 100644 src/kernel/types/agents/auth/invocation_create_params.py rename src/kernel/types/agents/{agent_auth_start_response.py => auth_agent_invocation_create_response.py} (69%) rename src/kernel/types/agents/{auth_start_params.py => auth_create_params.py} (61%) create mode 100644 src/kernel/types/agents/auth_list_params.py diff --git a/.stats.yml b/.stats.yml index 0c474bba..4bf353aa 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 80 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-8a37652fa586b8932466d16285359a89988505f850787f8257d0c4c7053da173.yml -openapi_spec_hash: 042765a113f6d08109e8146b302323ec -config_hash: 113f1e5bc3567628a5d51c70bc00969d +configured_endpoints: 82 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-b6957db438b01d979b62de21d4e674601b37d55b850b95a6e2b4c771aad5e840.yml +openapi_spec_hash: 1c8aac8322bc9df8f1b82a7e7a0c692b +config_hash: a4b4d14bdf6af723b235a6981977627c diff --git a/api.md b/api.md index d6384dcb..2876b33c 100644 --- a/api.md +++ b/api.md @@ -291,17 +291,20 @@ Types: from kernel.types.agents import ( AgentAuthDiscoverResponse, AgentAuthInvocationResponse, - AgentAuthStartResponse, AgentAuthSubmitResponse, AuthAgent, + AuthAgentCreateRequest, + AuthAgentInvocationCreateRequest, + AuthAgentInvocationCreateResponse, DiscoveredField, ) ``` Methods: +- client.agents.auth.create(\*\*params) -> AuthAgent - client.agents.auth.retrieve(id) -> AuthAgent -- client.agents.auth.start(\*\*params) -> AgentAuthStartResponse +- client.agents.auth.list(\*\*params) -> SyncOffsetPagination[AuthAgent] ### Invocations @@ -313,6 +316,7 @@ from kernel.types.agents.auth import InvocationExchangeResponse Methods: +- client.agents.auth.invocations.create(\*\*params) -> AuthAgentInvocationCreateResponse - client.agents.auth.invocations.retrieve(invocation_id) -> AgentAuthInvocationResponse - client.agents.auth.invocations.discover(invocation_id, \*\*params) -> AgentAuthDiscoverResponse - client.agents.auth.invocations.exchange(invocation_id, \*\*params) -> InvocationExchangeResponse diff --git a/src/kernel/resources/agents/auth/auth.py b/src/kernel/resources/agents/auth/auth.py index daa82210..b4fc7588 100644 --- a/src/kernel/resources/agents/auth/auth.py +++ b/src/kernel/resources/agents/auth/auth.py @@ -22,10 +22,10 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from ...._base_client import make_request_options -from ....types.agents import auth_start_params +from ....pagination import SyncOffsetPagination, AsyncOffsetPagination +from ...._base_client import AsyncPaginator, make_request_options +from ....types.agents import auth_list_params, auth_create_params from ....types.agents.auth_agent import AuthAgent -from ....types.agents.agent_auth_start_response import AgentAuthStartResponse __all__ = ["AuthResource", "AsyncAuthResource"] @@ -54,6 +54,61 @@ def with_streaming_response(self) -> AuthResourceWithStreamingResponse: """ return AuthResourceWithStreamingResponse(self) + def create( + self, + *, + profile_name: str, + target_domain: str, + login_url: str | Omit = omit, + proxy: auth_create_params.Proxy | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AuthAgent: + """ + Creates a new auth agent for the specified domain and profile combination, or + returns an existing one if it already exists. This is idempotent - calling with + the same domain and profile will return the same agent. Does NOT start an + invocation - use POST /agents/auth/invocations to start an auth flow. + + Args: + profile_name: Name of the profile to use for this auth agent + + target_domain: Target domain for authentication + + login_url: Optional login page URL. If provided, will be stored on the agent and used to + skip discovery in future invocations. + + proxy: Optional proxy configuration + + 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( + "/agents/auth", + body=maybe_transform( + { + "profile_name": profile_name, + "target_domain": target_domain, + "login_url": login_url, + "proxy": proxy, + }, + auth_create_params.AuthCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AuthAgent, + ) + def retrieve( self, id: str, @@ -89,38 +144,31 @@ def retrieve( cast_to=AuthAgent, ) - def start( + def list( self, *, - profile_name: str, - target_domain: str, - app_logo_url: str | Omit = omit, - login_url: str | Omit = omit, - proxy: auth_start_params.Proxy | Omit = omit, + limit: int | Omit = omit, + offset: int | Omit = omit, + profile_name: str | Omit = omit, + target_domain: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> AgentAuthStartResponse: - """Creates a browser session and returns a handoff code for the hosted flow. - - Uses - standard API key or JWT authentication (not the JWT returned by the exchange - endpoint). + ) -> SyncOffsetPagination[AuthAgent]: + """ + List auth agents with optional filters for profile_name and target_domain. Args: - profile_name: Name of the profile to use for this flow + limit: Maximum number of results to return - target_domain: Target domain for authentication - - app_logo_url: Optional logo URL for the application + offset: Number of results to skip - login_url: Optional login page URL. If provided, will be stored on the agent and used to - skip Phase 1 discovery in future invocations. + profile_name: Filter by profile name - proxy: Optional proxy configuration + target_domain: Filter by target domain extra_headers: Send extra headers @@ -130,22 +178,25 @@ def start( timeout: Override the client-level default timeout for this request, in seconds """ - return self._post( - "/agents/auth/start", - body=maybe_transform( - { - "profile_name": profile_name, - "target_domain": target_domain, - "app_logo_url": app_logo_url, - "login_url": login_url, - "proxy": proxy, - }, - auth_start_params.AuthStartParams, - ), + return self._get_api_list( + "/agents/auth", + page=SyncOffsetPagination[AuthAgent], options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "offset": offset, + "profile_name": profile_name, + "target_domain": target_domain, + }, + auth_list_params.AuthListParams, + ), ), - cast_to=AgentAuthStartResponse, + model=AuthAgent, ) @@ -173,6 +224,61 @@ def with_streaming_response(self) -> AsyncAuthResourceWithStreamingResponse: """ return AsyncAuthResourceWithStreamingResponse(self) + async def create( + self, + *, + profile_name: str, + target_domain: str, + login_url: str | Omit = omit, + proxy: auth_create_params.Proxy | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AuthAgent: + """ + Creates a new auth agent for the specified domain and profile combination, or + returns an existing one if it already exists. This is idempotent - calling with + the same domain and profile will return the same agent. Does NOT start an + invocation - use POST /agents/auth/invocations to start an auth flow. + + Args: + profile_name: Name of the profile to use for this auth agent + + target_domain: Target domain for authentication + + login_url: Optional login page URL. If provided, will be stored on the agent and used to + skip discovery in future invocations. + + proxy: Optional proxy configuration + + 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( + "/agents/auth", + body=await async_maybe_transform( + { + "profile_name": profile_name, + "target_domain": target_domain, + "login_url": login_url, + "proxy": proxy, + }, + auth_create_params.AuthCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AuthAgent, + ) + async def retrieve( self, id: str, @@ -208,38 +314,31 @@ async def retrieve( cast_to=AuthAgent, ) - async def start( + def list( self, *, - profile_name: str, - target_domain: str, - app_logo_url: str | Omit = omit, - login_url: str | Omit = omit, - proxy: auth_start_params.Proxy | Omit = omit, + limit: int | Omit = omit, + offset: int | Omit = omit, + profile_name: str | Omit = omit, + target_domain: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> AgentAuthStartResponse: - """Creates a browser session and returns a handoff code for the hosted flow. - - Uses - standard API key or JWT authentication (not the JWT returned by the exchange - endpoint). + ) -> AsyncPaginator[AuthAgent, AsyncOffsetPagination[AuthAgent]]: + """ + List auth agents with optional filters for profile_name and target_domain. Args: - profile_name: Name of the profile to use for this flow + limit: Maximum number of results to return - target_domain: Target domain for authentication + offset: Number of results to skip - app_logo_url: Optional logo URL for the application + profile_name: Filter by profile name - login_url: Optional login page URL. If provided, will be stored on the agent and used to - skip Phase 1 discovery in future invocations. - - proxy: Optional proxy configuration + target_domain: Filter by target domain extra_headers: Send extra headers @@ -249,22 +348,25 @@ async def start( timeout: Override the client-level default timeout for this request, in seconds """ - return await self._post( - "/agents/auth/start", - body=await async_maybe_transform( - { - "profile_name": profile_name, - "target_domain": target_domain, - "app_logo_url": app_logo_url, - "login_url": login_url, - "proxy": proxy, - }, - auth_start_params.AuthStartParams, - ), + return self._get_api_list( + "/agents/auth", + page=AsyncOffsetPagination[AuthAgent], options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "offset": offset, + "profile_name": profile_name, + "target_domain": target_domain, + }, + auth_list_params.AuthListParams, + ), ), - cast_to=AgentAuthStartResponse, + model=AuthAgent, ) @@ -272,11 +374,14 @@ class AuthResourceWithRawResponse: def __init__(self, auth: AuthResource) -> None: self._auth = auth + self.create = to_raw_response_wrapper( + auth.create, + ) self.retrieve = to_raw_response_wrapper( auth.retrieve, ) - self.start = to_raw_response_wrapper( - auth.start, + self.list = to_raw_response_wrapper( + auth.list, ) @cached_property @@ -288,11 +393,14 @@ class AsyncAuthResourceWithRawResponse: def __init__(self, auth: AsyncAuthResource) -> None: self._auth = auth + self.create = async_to_raw_response_wrapper( + auth.create, + ) self.retrieve = async_to_raw_response_wrapper( auth.retrieve, ) - self.start = async_to_raw_response_wrapper( - auth.start, + self.list = async_to_raw_response_wrapper( + auth.list, ) @cached_property @@ -304,11 +412,14 @@ class AuthResourceWithStreamingResponse: def __init__(self, auth: AuthResource) -> None: self._auth = auth + self.create = to_streamed_response_wrapper( + auth.create, + ) self.retrieve = to_streamed_response_wrapper( auth.retrieve, ) - self.start = to_streamed_response_wrapper( - auth.start, + self.list = to_streamed_response_wrapper( + auth.list, ) @cached_property @@ -320,11 +431,14 @@ class AsyncAuthResourceWithStreamingResponse: def __init__(self, auth: AsyncAuthResource) -> None: self._auth = auth + self.create = async_to_streamed_response_wrapper( + auth.create, + ) self.retrieve = async_to_streamed_response_wrapper( auth.retrieve, ) - self.start = async_to_streamed_response_wrapper( - auth.start, + self.list = async_to_streamed_response_wrapper( + auth.list, ) @cached_property diff --git a/src/kernel/resources/agents/auth/invocations.py b/src/kernel/resources/agents/auth/invocations.py index 15729ed8..361f4242 100644 --- a/src/kernel/resources/agents/auth/invocations.py +++ b/src/kernel/resources/agents/auth/invocations.py @@ -17,11 +17,17 @@ async_to_streamed_response_wrapper, ) from ...._base_client import make_request_options -from ....types.agents.auth import invocation_submit_params, invocation_discover_params, invocation_exchange_params +from ....types.agents.auth import ( + invocation_create_params, + invocation_submit_params, + invocation_discover_params, + invocation_exchange_params, +) from ....types.agents.agent_auth_submit_response import AgentAuthSubmitResponse from ....types.agents.agent_auth_discover_response import AgentAuthDiscoverResponse from ....types.agents.agent_auth_invocation_response import AgentAuthInvocationResponse from ....types.agents.auth.invocation_exchange_response import InvocationExchangeResponse +from ....types.agents.auth_agent_invocation_create_response import AuthAgentInvocationCreateResponse __all__ = ["InvocationsResource", "AsyncInvocationsResource"] @@ -46,6 +52,43 @@ def with_streaming_response(self) -> InvocationsResourceWithStreamingResponse: """ return InvocationsResourceWithStreamingResponse(self) + def create( + self, + *, + auth_agent_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, + ) -> AuthAgentInvocationCreateResponse: + """Creates a new authentication invocation for the specified auth agent. + + This + starts the auth flow and returns a hosted URL for the user to complete + authentication. + + Args: + auth_agent_id: ID of the auth agent to create an invocation for + + 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( + "/agents/auth/invocations", + body=maybe_transform({"auth_agent_id": auth_agent_id}, invocation_create_params.InvocationCreateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AuthAgentInvocationCreateResponse, + ) + def retrieve( self, invocation_id: str, @@ -219,6 +262,45 @@ def with_streaming_response(self) -> AsyncInvocationsResourceWithStreamingRespon """ return AsyncInvocationsResourceWithStreamingResponse(self) + async def create( + self, + *, + auth_agent_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, + ) -> AuthAgentInvocationCreateResponse: + """Creates a new authentication invocation for the specified auth agent. + + This + starts the auth flow and returns a hosted URL for the user to complete + authentication. + + Args: + auth_agent_id: ID of the auth agent to create an invocation for + + 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( + "/agents/auth/invocations", + body=await async_maybe_transform( + {"auth_agent_id": auth_agent_id}, invocation_create_params.InvocationCreateParams + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AuthAgentInvocationCreateResponse, + ) + async def retrieve( self, invocation_id: str, @@ -380,6 +462,9 @@ class InvocationsResourceWithRawResponse: def __init__(self, invocations: InvocationsResource) -> None: self._invocations = invocations + self.create = to_raw_response_wrapper( + invocations.create, + ) self.retrieve = to_raw_response_wrapper( invocations.retrieve, ) @@ -398,6 +483,9 @@ class AsyncInvocationsResourceWithRawResponse: def __init__(self, invocations: AsyncInvocationsResource) -> None: self._invocations = invocations + self.create = async_to_raw_response_wrapper( + invocations.create, + ) self.retrieve = async_to_raw_response_wrapper( invocations.retrieve, ) @@ -416,6 +504,9 @@ class InvocationsResourceWithStreamingResponse: def __init__(self, invocations: InvocationsResource) -> None: self._invocations = invocations + self.create = to_streamed_response_wrapper( + invocations.create, + ) self.retrieve = to_streamed_response_wrapper( invocations.retrieve, ) @@ -434,6 +525,9 @@ class AsyncInvocationsResourceWithStreamingResponse: def __init__(self, invocations: AsyncInvocationsResource) -> None: self._invocations = invocations + self.create = async_to_streamed_response_wrapper( + invocations.create, + ) self.retrieve = async_to_streamed_response_wrapper( invocations.retrieve, ) diff --git a/src/kernel/types/agents/__init__.py b/src/kernel/types/agents/__init__.py index 1fdcc09b..686e805a 100644 --- a/src/kernel/types/agents/__init__.py +++ b/src/kernel/types/agents/__init__.py @@ -3,9 +3,12 @@ from __future__ import annotations from .auth_agent import AuthAgent as AuthAgent +from .auth_list_params import AuthListParams as AuthListParams from .discovered_field import DiscoveredField as DiscoveredField -from .auth_start_params import AuthStartParams as AuthStartParams -from .agent_auth_start_response import AgentAuthStartResponse as AgentAuthStartResponse +from .auth_create_params import AuthCreateParams as AuthCreateParams from .agent_auth_submit_response import AgentAuthSubmitResponse as AgentAuthSubmitResponse from .agent_auth_discover_response import AgentAuthDiscoverResponse as AgentAuthDiscoverResponse from .agent_auth_invocation_response import AgentAuthInvocationResponse as AgentAuthInvocationResponse +from .auth_agent_invocation_create_response import ( + AuthAgentInvocationCreateResponse as AuthAgentInvocationCreateResponse, +) diff --git a/src/kernel/types/agents/auth/__init__.py b/src/kernel/types/agents/auth/__init__.py index bfbd2801..02968833 100644 --- a/src/kernel/types/agents/auth/__init__.py +++ b/src/kernel/types/agents/auth/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations +from .invocation_create_params import InvocationCreateParams as InvocationCreateParams from .invocation_submit_params import InvocationSubmitParams as InvocationSubmitParams from .invocation_discover_params import InvocationDiscoverParams as InvocationDiscoverParams from .invocation_exchange_params import InvocationExchangeParams as InvocationExchangeParams diff --git a/src/kernel/types/agents/auth/invocation_create_params.py b/src/kernel/types/agents/auth/invocation_create_params.py new file mode 100644 index 00000000..b3de645e --- /dev/null +++ b/src/kernel/types/agents/auth/invocation_create_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["InvocationCreateParams"] + + +class InvocationCreateParams(TypedDict, total=False): + auth_agent_id: Required[str] + """ID of the auth agent to create an invocation for""" diff --git a/src/kernel/types/agents/agent_auth_start_response.py b/src/kernel/types/agents/auth_agent_invocation_create_response.py similarity index 69% rename from src/kernel/types/agents/agent_auth_start_response.py rename to src/kernel/types/agents/auth_agent_invocation_create_response.py index 3287ba0b..0283c35f 100644 --- a/src/kernel/types/agents/agent_auth_start_response.py +++ b/src/kernel/types/agents/auth_agent_invocation_create_response.py @@ -4,13 +4,10 @@ from ..._models import BaseModel -__all__ = ["AgentAuthStartResponse"] +__all__ = ["AuthAgentInvocationCreateResponse"] -class AgentAuthStartResponse(BaseModel): - auth_agent_id: str - """Unique identifier for the auth agent managing this domain/profile""" - +class AuthAgentInvocationCreateResponse(BaseModel): expires_at: datetime """When the handoff code expires""" diff --git a/src/kernel/types/agents/auth_start_params.py b/src/kernel/types/agents/auth_create_params.py similarity index 61% rename from src/kernel/types/agents/auth_start_params.py rename to src/kernel/types/agents/auth_create_params.py index 9c9fb35a..fe577302 100644 --- a/src/kernel/types/agents/auth_start_params.py +++ b/src/kernel/types/agents/auth_create_params.py @@ -4,24 +4,21 @@ from typing_extensions import Required, TypedDict -__all__ = ["AuthStartParams", "Proxy"] +__all__ = ["AuthCreateParams", "Proxy"] -class AuthStartParams(TypedDict, total=False): +class AuthCreateParams(TypedDict, total=False): profile_name: Required[str] - """Name of the profile to use for this flow""" + """Name of the profile to use for this auth agent""" target_domain: Required[str] """Target domain for authentication""" - app_logo_url: str - """Optional logo URL for the application""" - login_url: str """Optional login page URL. - If provided, will be stored on the agent and used to skip Phase 1 discovery in - future invocations. + If provided, will be stored on the agent and used to skip discovery in future + invocations. """ proxy: Proxy diff --git a/src/kernel/types/agents/auth_list_params.py b/src/kernel/types/agents/auth_list_params.py new file mode 100644 index 00000000..a4b2ffcb --- /dev/null +++ b/src/kernel/types/agents/auth_list_params.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["AuthListParams"] + + +class AuthListParams(TypedDict, total=False): + limit: int + """Maximum number of results to return""" + + offset: int + """Number of results to skip""" + + profile_name: str + """Filter by profile name""" + + target_domain: str + """Filter by target domain""" diff --git a/tests/api_resources/agents/auth/test_invocations.py b/tests/api_resources/agents/auth/test_invocations.py index e9c3d8f7..7957caf5 100644 --- a/tests/api_resources/agents/auth/test_invocations.py +++ b/tests/api_resources/agents/auth/test_invocations.py @@ -9,7 +9,12 @@ from kernel import Kernel, AsyncKernel from tests.utils import assert_matches_type -from kernel.types.agents import AgentAuthSubmitResponse, AgentAuthDiscoverResponse, AgentAuthInvocationResponse +from kernel.types.agents import ( + AgentAuthSubmitResponse, + AgentAuthDiscoverResponse, + AgentAuthInvocationResponse, + AuthAgentInvocationCreateResponse, +) from kernel.types.agents.auth import ( InvocationExchangeResponse, ) @@ -20,6 +25,40 @@ class TestInvocations: 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: Kernel) -> None: + invocation = client.agents.auth.invocations.create( + auth_agent_id="abc123xyz", + ) + assert_matches_type(AuthAgentInvocationCreateResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_create(self, client: Kernel) -> None: + response = client.agents.auth.invocations.with_raw_response.create( + auth_agent_id="abc123xyz", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + invocation = response.parse() + assert_matches_type(AuthAgentInvocationCreateResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_create(self, client: Kernel) -> None: + with client.agents.auth.invocations.with_streaming_response.create( + auth_agent_id="abc123xyz", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + invocation = response.parse() + assert_matches_type(AuthAgentInvocationCreateResponse, invocation, 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: Kernel) -> None: @@ -223,6 +262,40 @@ class TestAsyncInvocations: "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: AsyncKernel) -> None: + invocation = await async_client.agents.auth.invocations.create( + auth_agent_id="abc123xyz", + ) + assert_matches_type(AuthAgentInvocationCreateResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_create(self, async_client: AsyncKernel) -> None: + response = await async_client.agents.auth.invocations.with_raw_response.create( + auth_agent_id="abc123xyz", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + invocation = await response.parse() + assert_matches_type(AuthAgentInvocationCreateResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: + async with async_client.agents.auth.invocations.with_streaming_response.create( + auth_agent_id="abc123xyz", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + invocation = await response.parse() + assert_matches_type(AuthAgentInvocationCreateResponse, invocation, 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: AsyncKernel) -> None: diff --git a/tests/api_resources/agents/test_auth.py b/tests/api_resources/agents/test_auth.py index 0dc190d8..5115f901 100644 --- a/tests/api_resources/agents/test_auth.py +++ b/tests/api_resources/agents/test_auth.py @@ -9,7 +9,8 @@ from kernel import Kernel, AsyncKernel from tests.utils import assert_matches_type -from kernel.types.agents import AuthAgent, AgentAuthStartResponse +from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination +from kernel.types.agents import AuthAgent base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -17,6 +18,54 @@ class TestAuth: 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: Kernel) -> None: + auth = client.agents.auth.create( + profile_name="user-123", + target_domain="netflix.com", + ) + assert_matches_type(AuthAgent, auth, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_create_with_all_params(self, client: Kernel) -> None: + auth = client.agents.auth.create( + profile_name="user-123", + target_domain="netflix.com", + login_url="https://netflix.com/login", + proxy={"proxy_id": "proxy_id"}, + ) + assert_matches_type(AuthAgent, auth, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_create(self, client: Kernel) -> None: + response = client.agents.auth.with_raw_response.create( + profile_name="user-123", + target_domain="netflix.com", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + auth = response.parse() + assert_matches_type(AuthAgent, auth, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_create(self, client: Kernel) -> None: + with client.agents.auth.with_streaming_response.create( + profile_name="user-123", + target_domain="netflix.com", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + auth = response.parse() + assert_matches_type(AuthAgent, auth, 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: Kernel) -> None: @@ -61,50 +110,40 @@ def test_path_params_retrieve(self, client: Kernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_start(self, client: Kernel) -> None: - auth = client.agents.auth.start( - profile_name="auth-abc123", - target_domain="doordash.com", - ) - assert_matches_type(AgentAuthStartResponse, auth, path=["response"]) + def test_method_list(self, client: Kernel) -> None: + auth = client.agents.auth.list() + assert_matches_type(SyncOffsetPagination[AuthAgent], auth, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_start_with_all_params(self, client: Kernel) -> None: - auth = client.agents.auth.start( - profile_name="auth-abc123", - target_domain="doordash.com", - app_logo_url="https://example.com/logo.png", - login_url="https://doordash.com/account/login", - proxy={"proxy_id": "proxy_id"}, + def test_method_list_with_all_params(self, client: Kernel) -> None: + auth = client.agents.auth.list( + limit=100, + offset=0, + profile_name="profile_name", + target_domain="target_domain", ) - assert_matches_type(AgentAuthStartResponse, auth, path=["response"]) + assert_matches_type(SyncOffsetPagination[AuthAgent], auth, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_raw_response_start(self, client: Kernel) -> None: - response = client.agents.auth.with_raw_response.start( - profile_name="auth-abc123", - target_domain="doordash.com", - ) + def test_raw_response_list(self, client: Kernel) -> None: + response = client.agents.auth.with_raw_response.list() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" auth = response.parse() - assert_matches_type(AgentAuthStartResponse, auth, path=["response"]) + assert_matches_type(SyncOffsetPagination[AuthAgent], auth, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_streaming_response_start(self, client: Kernel) -> None: - with client.agents.auth.with_streaming_response.start( - profile_name="auth-abc123", - target_domain="doordash.com", - ) as response: + def test_streaming_response_list(self, client: Kernel) -> None: + with client.agents.auth.with_streaming_response.list() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" auth = response.parse() - assert_matches_type(AgentAuthStartResponse, auth, path=["response"]) + assert_matches_type(SyncOffsetPagination[AuthAgent], auth, path=["response"]) assert cast(Any, response.is_closed) is True @@ -114,6 +153,54 @@ class TestAsyncAuth: "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: AsyncKernel) -> None: + auth = await async_client.agents.auth.create( + profile_name="user-123", + target_domain="netflix.com", + ) + assert_matches_type(AuthAgent, auth, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: + auth = await async_client.agents.auth.create( + profile_name="user-123", + target_domain="netflix.com", + login_url="https://netflix.com/login", + proxy={"proxy_id": "proxy_id"}, + ) + assert_matches_type(AuthAgent, auth, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_create(self, async_client: AsyncKernel) -> None: + response = await async_client.agents.auth.with_raw_response.create( + profile_name="user-123", + target_domain="netflix.com", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + auth = await response.parse() + assert_matches_type(AuthAgent, auth, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: + async with async_client.agents.auth.with_streaming_response.create( + profile_name="user-123", + target_domain="netflix.com", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + auth = await response.parse() + assert_matches_type(AuthAgent, auth, 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: AsyncKernel) -> None: @@ -158,49 +245,39 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_start(self, async_client: AsyncKernel) -> None: - auth = await async_client.agents.auth.start( - profile_name="auth-abc123", - target_domain="doordash.com", - ) - assert_matches_type(AgentAuthStartResponse, auth, path=["response"]) + async def test_method_list(self, async_client: AsyncKernel) -> None: + auth = await async_client.agents.auth.list() + assert_matches_type(AsyncOffsetPagination[AuthAgent], auth, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_start_with_all_params(self, async_client: AsyncKernel) -> None: - auth = await async_client.agents.auth.start( - profile_name="auth-abc123", - target_domain="doordash.com", - app_logo_url="https://example.com/logo.png", - login_url="https://doordash.com/account/login", - proxy={"proxy_id": "proxy_id"}, + async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: + auth = await async_client.agents.auth.list( + limit=100, + offset=0, + profile_name="profile_name", + target_domain="target_domain", ) - assert_matches_type(AgentAuthStartResponse, auth, path=["response"]) + assert_matches_type(AsyncOffsetPagination[AuthAgent], auth, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_raw_response_start(self, async_client: AsyncKernel) -> None: - response = await async_client.agents.auth.with_raw_response.start( - profile_name="auth-abc123", - target_domain="doordash.com", - ) + async def test_raw_response_list(self, async_client: AsyncKernel) -> None: + response = await async_client.agents.auth.with_raw_response.list() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" auth = await response.parse() - assert_matches_type(AgentAuthStartResponse, auth, path=["response"]) + assert_matches_type(AsyncOffsetPagination[AuthAgent], auth, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_streaming_response_start(self, async_client: AsyncKernel) -> None: - async with async_client.agents.auth.with_streaming_response.start( - profile_name="auth-abc123", - target_domain="doordash.com", - ) as response: + async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: + async with async_client.agents.auth.with_streaming_response.list() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" auth = await response.parse() - assert_matches_type(AgentAuthStartResponse, auth, path=["response"]) + assert_matches_type(AsyncOffsetPagination[AuthAgent], auth, path=["response"]) assert cast(Any, response.is_closed) is True From 94a95ffef8229c6777ec7589ca5bb4d82e769b07 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 03:42:24 +0000 Subject: [PATCH 235/448] fix(types): allow pyright to infer TypedDict types within SequenceNotStr --- src/kernel/_types.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/kernel/_types.py b/src/kernel/_types.py index 2c1d83b2..275ffbbc 100644 --- a/src/kernel/_types.py +++ b/src/kernel/_types.py @@ -243,6 +243,9 @@ class HttpxSendArgs(TypedDict, total=False): if TYPE_CHECKING: # This works because str.__contains__ does not accept object (either in typeshed or at runtime) # https://github.com/hauntsaninja/useful_types/blob/5e9710f3875107d068e7679fd7fec9cfab0eff3b/useful_types/__init__.py#L285 + # + # Note: index() and count() methods are intentionally omitted to allow pyright to properly + # infer TypedDict types when dict literals are used in lists assigned to SequenceNotStr. class SequenceNotStr(Protocol[_T_co]): @overload def __getitem__(self, index: SupportsIndex, /) -> _T_co: ... @@ -251,8 +254,6 @@ def __getitem__(self, index: slice, /) -> Sequence[_T_co]: ... def __contains__(self, value: object, /) -> bool: ... def __len__(self) -> int: ... def __iter__(self) -> Iterator[_T_co]: ... - def index(self, value: Any, start: int = 0, stop: int = ..., /) -> int: ... - def count(self, value: Any, /) -> int: ... def __reversed__(self) -> Iterator[_T_co]: ... else: # just point this to a normal `Sequence` at runtime to avoid having to special case From 924562d1e004de9ccfa1dd53ef45afd5d77ee1d4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 03:44:08 +0000 Subject: [PATCH 236/448] chore: add missing docstrings --- .../types/agents/agent_auth_discover_response.py | 2 ++ .../types/agents/agent_auth_invocation_response.py | 2 ++ .../types/agents/agent_auth_submit_response.py | 2 ++ .../agents/auth/invocation_exchange_response.py | 2 ++ src/kernel/types/agents/auth_agent.py | 4 ++++ .../agents/auth_agent_invocation_create_response.py | 2 ++ src/kernel/types/agents/auth_create_params.py | 2 ++ src/kernel/types/agents/discovered_field.py | 2 ++ src/kernel/types/app_list_response.py | 2 ++ src/kernel/types/browser_persistence.py | 2 ++ src/kernel/types/browser_persistence_param.py | 2 ++ src/kernel/types/browser_pool.py | 2 ++ src/kernel/types/browser_pool_request.py | 5 +++++ .../computer_set_cursor_visibility_response.py | 2 ++ .../types/browsers/fs/watch_events_response.py | 2 ++ .../types/browsers/playwright_execute_response.py | 2 ++ src/kernel/types/browsers/process_exec_response.py | 2 ++ src/kernel/types/browsers/process_kill_response.py | 2 ++ src/kernel/types/browsers/process_spawn_response.py | 2 ++ src/kernel/types/browsers/process_status_response.py | 2 ++ src/kernel/types/browsers/process_stdin_response.py | 2 ++ .../types/browsers/process_stdout_stream_response.py | 2 ++ src/kernel/types/browsers/replay_list_response.py | 2 ++ src/kernel/types/browsers/replay_start_response.py | 2 ++ src/kernel/types/deployment_create_params.py | 4 ++++ src/kernel/types/deployment_create_response.py | 2 ++ src/kernel/types/deployment_follow_response.py | 2 ++ src/kernel/types/deployment_list_response.py | 2 ++ src/kernel/types/deployment_retrieve_response.py | 2 ++ src/kernel/types/deployment_state_event.py | 4 ++++ src/kernel/types/extension_list_response.py | 2 ++ src/kernel/types/extension_upload_response.py | 2 ++ src/kernel/types/invocation_state_event.py | 2 ++ src/kernel/types/profile.py | 2 ++ src/kernel/types/proxy_create_params.py | 10 ++++++++++ src/kernel/types/proxy_create_response.py | 12 ++++++++++++ src/kernel/types/proxy_list_response.py | 12 ++++++++++++ src/kernel/types/proxy_retrieve_response.py | 12 ++++++++++++ src/kernel/types/shared/app_action.py | 2 ++ src/kernel/types/shared/browser_extension.py | 5 +++++ src/kernel/types/shared/browser_profile.py | 6 ++++++ src/kernel/types/shared/browser_viewport.py | 9 +++++++++ src/kernel/types/shared/error_event.py | 2 ++ src/kernel/types/shared/heartbeat_event.py | 2 ++ src/kernel/types/shared/log_event.py | 2 ++ src/kernel/types/shared_params/browser_extension.py | 5 +++++ src/kernel/types/shared_params/browser_profile.py | 6 ++++++ src/kernel/types/shared_params/browser_viewport.py | 9 +++++++++ 48 files changed, 171 insertions(+) diff --git a/src/kernel/types/agents/agent_auth_discover_response.py b/src/kernel/types/agents/agent_auth_discover_response.py index 000bdec2..5e411dcb 100644 --- a/src/kernel/types/agents/agent_auth_discover_response.py +++ b/src/kernel/types/agents/agent_auth_discover_response.py @@ -9,6 +9,8 @@ class AgentAuthDiscoverResponse(BaseModel): + """Response from discover endpoint matching AuthBlueprint schema""" + success: bool """Whether discovery succeeded""" diff --git a/src/kernel/types/agents/agent_auth_invocation_response.py b/src/kernel/types/agents/agent_auth_invocation_response.py index 82b5f80e..02b5ecf9 100644 --- a/src/kernel/types/agents/agent_auth_invocation_response.py +++ b/src/kernel/types/agents/agent_auth_invocation_response.py @@ -9,6 +9,8 @@ class AgentAuthInvocationResponse(BaseModel): + """Response from get invocation endpoint""" + app_name: str """App name (org name at time of invocation creation)""" diff --git a/src/kernel/types/agents/agent_auth_submit_response.py b/src/kernel/types/agents/agent_auth_submit_response.py index c57002fb..5ca9578c 100644 --- a/src/kernel/types/agents/agent_auth_submit_response.py +++ b/src/kernel/types/agents/agent_auth_submit_response.py @@ -9,6 +9,8 @@ class AgentAuthSubmitResponse(BaseModel): + """Response from submit endpoint matching SubmitResult schema""" + success: bool """Whether submission succeeded""" diff --git a/src/kernel/types/agents/auth/invocation_exchange_response.py b/src/kernel/types/agents/auth/invocation_exchange_response.py index 91b74ceb..710d9c31 100644 --- a/src/kernel/types/agents/auth/invocation_exchange_response.py +++ b/src/kernel/types/agents/auth/invocation_exchange_response.py @@ -6,6 +6,8 @@ class InvocationExchangeResponse(BaseModel): + """Response from exchange endpoint""" + invocation_id: str """Invocation ID""" diff --git a/src/kernel/types/agents/auth_agent.py b/src/kernel/types/agents/auth_agent.py index 8671f97c..ff9f5e9d 100644 --- a/src/kernel/types/agents/auth_agent.py +++ b/src/kernel/types/agents/auth_agent.py @@ -8,6 +8,10 @@ class AuthAgent(BaseModel): + """ + An auth agent that manages authentication for a specific domain and profile combination + """ + id: str """Unique identifier for the auth agent""" diff --git a/src/kernel/types/agents/auth_agent_invocation_create_response.py b/src/kernel/types/agents/auth_agent_invocation_create_response.py index 0283c35f..baa80e2a 100644 --- a/src/kernel/types/agents/auth_agent_invocation_create_response.py +++ b/src/kernel/types/agents/auth_agent_invocation_create_response.py @@ -8,6 +8,8 @@ class AuthAgentInvocationCreateResponse(BaseModel): + """Response from creating an auth agent invocation""" + expires_at: datetime """When the handoff code expires""" diff --git a/src/kernel/types/agents/auth_create_params.py b/src/kernel/types/agents/auth_create_params.py index fe577302..7869925e 100644 --- a/src/kernel/types/agents/auth_create_params.py +++ b/src/kernel/types/agents/auth_create_params.py @@ -26,5 +26,7 @@ class AuthCreateParams(TypedDict, total=False): class Proxy(TypedDict, total=False): + """Optional proxy configuration""" + proxy_id: str """ID of the proxy to use""" diff --git a/src/kernel/types/agents/discovered_field.py b/src/kernel/types/agents/discovered_field.py index d1b9dc9d..0c6715c1 100644 --- a/src/kernel/types/agents/discovered_field.py +++ b/src/kernel/types/agents/discovered_field.py @@ -9,6 +9,8 @@ class DiscoveredField(BaseModel): + """A discovered form field""" + label: str """Field label""" diff --git a/src/kernel/types/app_list_response.py b/src/kernel/types/app_list_response.py index 56a2d4bd..338f506d 100644 --- a/src/kernel/types/app_list_response.py +++ b/src/kernel/types/app_list_response.py @@ -10,6 +10,8 @@ class AppListResponse(BaseModel): + """Summary of an application version.""" + id: str """Unique identifier for the app version""" diff --git a/src/kernel/types/browser_persistence.py b/src/kernel/types/browser_persistence.py index 5c362eee..381d6306 100644 --- a/src/kernel/types/browser_persistence.py +++ b/src/kernel/types/browser_persistence.py @@ -6,5 +6,7 @@ class BrowserPersistence(BaseModel): + """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead.""" + id: str """DEPRECATED: Unique identifier for the persistent browser session.""" diff --git a/src/kernel/types/browser_persistence_param.py b/src/kernel/types/browser_persistence_param.py index bbd9e483..6109abfd 100644 --- a/src/kernel/types/browser_persistence_param.py +++ b/src/kernel/types/browser_persistence_param.py @@ -8,5 +8,7 @@ class BrowserPersistenceParam(TypedDict, total=False): + """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead.""" + id: Required[str] """DEPRECATED: Unique identifier for the persistent browser session.""" diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py index 5fd30dca..ddd3d9f4 100644 --- a/src/kernel/types/browser_pool.py +++ b/src/kernel/types/browser_pool.py @@ -10,6 +10,8 @@ class BrowserPool(BaseModel): + """A browser pool containing multiple identically configured browsers.""" + id: str """Unique identifier for the browser pool""" diff --git a/src/kernel/types/browser_pool_request.py b/src/kernel/types/browser_pool_request.py index c54fad41..a392b3fb 100644 --- a/src/kernel/types/browser_pool_request.py +++ b/src/kernel/types/browser_pool_request.py @@ -11,6 +11,11 @@ class BrowserPoolRequest(BaseModel): + """Parameters for creating a browser pool. + + All browsers in the pool will be created with the same configuration. + """ + size: int """Number of browsers to create in the pool""" diff --git a/src/kernel/types/browsers/computer_set_cursor_visibility_response.py b/src/kernel/types/browsers/computer_set_cursor_visibility_response.py index c82302ef..0e070239 100644 --- a/src/kernel/types/browsers/computer_set_cursor_visibility_response.py +++ b/src/kernel/types/browsers/computer_set_cursor_visibility_response.py @@ -6,5 +6,7 @@ class ComputerSetCursorVisibilityResponse(BaseModel): + """Generic OK response.""" + ok: bool """Indicates success.""" diff --git a/src/kernel/types/browsers/fs/watch_events_response.py b/src/kernel/types/browsers/fs/watch_events_response.py index 8df2f501..5778a309 100644 --- a/src/kernel/types/browsers/fs/watch_events_response.py +++ b/src/kernel/types/browsers/fs/watch_events_response.py @@ -9,6 +9,8 @@ class WatchEventsResponse(BaseModel): + """Filesystem change event.""" + path: str """Absolute path of the file or directory.""" diff --git a/src/kernel/types/browsers/playwright_execute_response.py b/src/kernel/types/browsers/playwright_execute_response.py index a805ba85..d53080de 100644 --- a/src/kernel/types/browsers/playwright_execute_response.py +++ b/src/kernel/types/browsers/playwright_execute_response.py @@ -8,6 +8,8 @@ class PlaywrightExecuteResponse(BaseModel): + """Result of Playwright code execution""" + success: bool """Whether the code executed successfully""" diff --git a/src/kernel/types/browsers/process_exec_response.py b/src/kernel/types/browsers/process_exec_response.py index 02588de0..a5e4b772 100644 --- a/src/kernel/types/browsers/process_exec_response.py +++ b/src/kernel/types/browsers/process_exec_response.py @@ -8,6 +8,8 @@ class ProcessExecResponse(BaseModel): + """Result of a synchronous command execution.""" + duration_ms: Optional[int] = None """Execution duration in milliseconds.""" diff --git a/src/kernel/types/browsers/process_kill_response.py b/src/kernel/types/browsers/process_kill_response.py index ed128a78..6706e88b 100644 --- a/src/kernel/types/browsers/process_kill_response.py +++ b/src/kernel/types/browsers/process_kill_response.py @@ -6,5 +6,7 @@ class ProcessKillResponse(BaseModel): + """Generic OK response.""" + ok: bool """Indicates success.""" diff --git a/src/kernel/types/browsers/process_spawn_response.py b/src/kernel/types/browsers/process_spawn_response.py index 23444da2..0cda64d5 100644 --- a/src/kernel/types/browsers/process_spawn_response.py +++ b/src/kernel/types/browsers/process_spawn_response.py @@ -9,6 +9,8 @@ class ProcessSpawnResponse(BaseModel): + """Information about a spawned process.""" + pid: Optional[int] = None """OS process ID.""" diff --git a/src/kernel/types/browsers/process_status_response.py b/src/kernel/types/browsers/process_status_response.py index 67626fe9..91c77240 100644 --- a/src/kernel/types/browsers/process_status_response.py +++ b/src/kernel/types/browsers/process_status_response.py @@ -9,6 +9,8 @@ class ProcessStatusResponse(BaseModel): + """Current status of a process.""" + cpu_pct: Optional[float] = None """Estimated CPU usage percentage.""" diff --git a/src/kernel/types/browsers/process_stdin_response.py b/src/kernel/types/browsers/process_stdin_response.py index d137a962..be3c7987 100644 --- a/src/kernel/types/browsers/process_stdin_response.py +++ b/src/kernel/types/browsers/process_stdin_response.py @@ -8,5 +8,7 @@ class ProcessStdinResponse(BaseModel): + """Result of writing to stdin.""" + written_bytes: Optional[int] = None """Number of bytes written.""" diff --git a/src/kernel/types/browsers/process_stdout_stream_response.py b/src/kernel/types/browsers/process_stdout_stream_response.py index 0b1d0a8e..6e911f5d 100644 --- a/src/kernel/types/browsers/process_stdout_stream_response.py +++ b/src/kernel/types/browsers/process_stdout_stream_response.py @@ -9,6 +9,8 @@ class ProcessStdoutStreamResponse(BaseModel): + """SSE payload representing process output or lifecycle events.""" + data_b64: Optional[str] = None """Base64-encoded data from the process stream.""" diff --git a/src/kernel/types/browsers/replay_list_response.py b/src/kernel/types/browsers/replay_list_response.py index f53dd4d4..8cf9d543 100644 --- a/src/kernel/types/browsers/replay_list_response.py +++ b/src/kernel/types/browsers/replay_list_response.py @@ -10,6 +10,8 @@ class ReplayListResponseItem(BaseModel): + """Information about a browser replay recording.""" + replay_id: str """Unique identifier for the replay recording.""" diff --git a/src/kernel/types/browsers/replay_start_response.py b/src/kernel/types/browsers/replay_start_response.py index dd837d50..ac4130b5 100644 --- a/src/kernel/types/browsers/replay_start_response.py +++ b/src/kernel/types/browsers/replay_start_response.py @@ -9,6 +9,8 @@ class ReplayStartResponse(BaseModel): + """Information about a browser replay recording.""" + replay_id: str """Unique identifier for the replay recording.""" diff --git a/src/kernel/types/deployment_create_params.py b/src/kernel/types/deployment_create_params.py index 16eb5702..84d3d876 100644 --- a/src/kernel/types/deployment_create_params.py +++ b/src/kernel/types/deployment_create_params.py @@ -37,6 +37,8 @@ class DeploymentCreateParams(TypedDict, total=False): class SourceAuth(TypedDict, total=False): + """Authentication for private repositories.""" + token: Required[str] """GitHub PAT or installation access token""" @@ -45,6 +47,8 @@ class SourceAuth(TypedDict, total=False): class Source(TypedDict, total=False): + """Source from which to fetch application code.""" + entrypoint: Required[str] """Relative path to the application entrypoint within the selected path.""" diff --git a/src/kernel/types/deployment_create_response.py b/src/kernel/types/deployment_create_response.py index c14bf273..5746c97b 100644 --- a/src/kernel/types/deployment_create_response.py +++ b/src/kernel/types/deployment_create_response.py @@ -10,6 +10,8 @@ class DeploymentCreateResponse(BaseModel): + """Deployment record information.""" + id: str """Unique identifier for the deployment""" diff --git a/src/kernel/types/deployment_follow_response.py b/src/kernel/types/deployment_follow_response.py index ca3c512a..d6de2223 100644 --- a/src/kernel/types/deployment_follow_response.py +++ b/src/kernel/types/deployment_follow_response.py @@ -16,6 +16,8 @@ class AppVersionSummaryEvent(BaseModel): + """Summary of an application version.""" + id: str """Unique identifier for the app version""" diff --git a/src/kernel/types/deployment_list_response.py b/src/kernel/types/deployment_list_response.py index d22b0076..d7719d48 100644 --- a/src/kernel/types/deployment_list_response.py +++ b/src/kernel/types/deployment_list_response.py @@ -10,6 +10,8 @@ class DeploymentListResponse(BaseModel): + """Deployment record information.""" + id: str """Unique identifier for the deployment""" diff --git a/src/kernel/types/deployment_retrieve_response.py b/src/kernel/types/deployment_retrieve_response.py index 28c0d4b9..3601c864 100644 --- a/src/kernel/types/deployment_retrieve_response.py +++ b/src/kernel/types/deployment_retrieve_response.py @@ -10,6 +10,8 @@ class DeploymentRetrieveResponse(BaseModel): + """Deployment record information.""" + id: str """Unique identifier for the deployment""" diff --git a/src/kernel/types/deployment_state_event.py b/src/kernel/types/deployment_state_event.py index 572d51bc..cc221c77 100644 --- a/src/kernel/types/deployment_state_event.py +++ b/src/kernel/types/deployment_state_event.py @@ -10,6 +10,8 @@ class Deployment(BaseModel): + """Deployment record information.""" + id: str """Unique identifier for the deployment""" @@ -36,6 +38,8 @@ class Deployment(BaseModel): class DeploymentStateEvent(BaseModel): + """An event representing the current state of a deployment.""" + deployment: Deployment """Deployment record information.""" diff --git a/src/kernel/types/extension_list_response.py b/src/kernel/types/extension_list_response.py index c8c99e71..79a5c991 100644 --- a/src/kernel/types/extension_list_response.py +++ b/src/kernel/types/extension_list_response.py @@ -10,6 +10,8 @@ class ExtensionListResponseItem(BaseModel): + """A browser extension uploaded to Kernel.""" + id: str """Unique identifier for the extension""" diff --git a/src/kernel/types/extension_upload_response.py b/src/kernel/types/extension_upload_response.py index 373e8861..1b3be221 100644 --- a/src/kernel/types/extension_upload_response.py +++ b/src/kernel/types/extension_upload_response.py @@ -9,6 +9,8 @@ class ExtensionUploadResponse(BaseModel): + """A browser extension uploaded to Kernel.""" + id: str """Unique identifier for the extension""" diff --git a/src/kernel/types/invocation_state_event.py b/src/kernel/types/invocation_state_event.py index 48a2fa30..f32bf8e9 100644 --- a/src/kernel/types/invocation_state_event.py +++ b/src/kernel/types/invocation_state_event.py @@ -51,6 +51,8 @@ class Invocation(BaseModel): class InvocationStateEvent(BaseModel): + """An event representing the current state of an invocation.""" + event: Literal["invocation_state"] """Event type identifier (always "invocation_state").""" diff --git a/src/kernel/types/profile.py b/src/kernel/types/profile.py index 3ec58903..e141aa06 100644 --- a/src/kernel/types/profile.py +++ b/src/kernel/types/profile.py @@ -9,6 +9,8 @@ class Profile(BaseModel): + """Browser profile metadata.""" + id: str """Unique identifier for the profile""" diff --git a/src/kernel/types/proxy_create_params.py b/src/kernel/types/proxy_create_params.py index 485df606..0a3536f0 100644 --- a/src/kernel/types/proxy_create_params.py +++ b/src/kernel/types/proxy_create_params.py @@ -35,16 +35,22 @@ class ProxyCreateParams(TypedDict, total=False): class ConfigDatacenterProxyConfig(TypedDict, total=False): + """Configuration for a datacenter proxy.""" + country: str """ISO 3166 country code. Defaults to US if not provided.""" class ConfigIspProxyConfig(TypedDict, total=False): + """Configuration for an ISP proxy.""" + country: str """ISO 3166 country code. Defaults to US if not provided.""" class ConfigResidentialProxyConfig(TypedDict, total=False): + """Configuration for residential proxies.""" + asn: str """Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html""" @@ -68,6 +74,8 @@ class ConfigResidentialProxyConfig(TypedDict, total=False): class ConfigMobileProxyConfig(TypedDict, total=False): + """Configuration for mobile proxies.""" + asn: str """Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html""" @@ -150,6 +158,8 @@ class ConfigMobileProxyConfig(TypedDict, total=False): class ConfigCreateCustomProxyConfig(TypedDict, total=False): + """Configuration for a custom proxy (e.g., private proxy server).""" + host: Required[str] """Proxy host address or IP.""" diff --git a/src/kernel/types/proxy_create_response.py b/src/kernel/types/proxy_create_response.py index 6ec2f7f9..dc474ab2 100644 --- a/src/kernel/types/proxy_create_response.py +++ b/src/kernel/types/proxy_create_response.py @@ -18,16 +18,22 @@ class ConfigDatacenterProxyConfig(BaseModel): + """Configuration for a datacenter proxy.""" + country: Optional[str] = None """ISO 3166 country code. Defaults to US if not provided.""" class ConfigIspProxyConfig(BaseModel): + """Configuration for an ISP proxy.""" + country: Optional[str] = None """ISO 3166 country code. Defaults to US if not provided.""" class ConfigResidentialProxyConfig(BaseModel): + """Configuration for residential proxies.""" + asn: Optional[str] = None """Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html""" @@ -51,6 +57,8 @@ class ConfigResidentialProxyConfig(BaseModel): class ConfigMobileProxyConfig(BaseModel): + """Configuration for mobile proxies.""" + asn: Optional[str] = None """Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html""" @@ -135,6 +143,8 @@ class ConfigMobileProxyConfig(BaseModel): class ConfigCustomProxyConfig(BaseModel): + """Configuration for a custom proxy (e.g., private proxy server).""" + host: str """Proxy host address or IP.""" @@ -158,6 +168,8 @@ class ConfigCustomProxyConfig(BaseModel): class ProxyCreateResponse(BaseModel): + """Configuration for routing traffic through a proxy.""" + type: Literal["datacenter", "isp", "residential", "mobile", "custom"] """Proxy type to use. diff --git a/src/kernel/types/proxy_list_response.py b/src/kernel/types/proxy_list_response.py index e4abb0d8..08c846f0 100644 --- a/src/kernel/types/proxy_list_response.py +++ b/src/kernel/types/proxy_list_response.py @@ -19,16 +19,22 @@ class ProxyListResponseItemConfigDatacenterProxyConfig(BaseModel): + """Configuration for a datacenter proxy.""" + country: Optional[str] = None """ISO 3166 country code. Defaults to US if not provided.""" class ProxyListResponseItemConfigIspProxyConfig(BaseModel): + """Configuration for an ISP proxy.""" + country: Optional[str] = None """ISO 3166 country code. Defaults to US if not provided.""" class ProxyListResponseItemConfigResidentialProxyConfig(BaseModel): + """Configuration for residential proxies.""" + asn: Optional[str] = None """Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html""" @@ -52,6 +58,8 @@ class ProxyListResponseItemConfigResidentialProxyConfig(BaseModel): class ProxyListResponseItemConfigMobileProxyConfig(BaseModel): + """Configuration for mobile proxies.""" + asn: Optional[str] = None """Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html""" @@ -136,6 +144,8 @@ class ProxyListResponseItemConfigMobileProxyConfig(BaseModel): class ProxyListResponseItemConfigCustomProxyConfig(BaseModel): + """Configuration for a custom proxy (e.g., private proxy server).""" + host: str """Proxy host address or IP.""" @@ -159,6 +169,8 @@ class ProxyListResponseItemConfigCustomProxyConfig(BaseModel): class ProxyListResponseItem(BaseModel): + """Configuration for routing traffic through a proxy.""" + type: Literal["datacenter", "isp", "residential", "mobile", "custom"] """Proxy type to use. diff --git a/src/kernel/types/proxy_retrieve_response.py b/src/kernel/types/proxy_retrieve_response.py index 5262fc48..24c7b96f 100644 --- a/src/kernel/types/proxy_retrieve_response.py +++ b/src/kernel/types/proxy_retrieve_response.py @@ -18,16 +18,22 @@ class ConfigDatacenterProxyConfig(BaseModel): + """Configuration for a datacenter proxy.""" + country: Optional[str] = None """ISO 3166 country code. Defaults to US if not provided.""" class ConfigIspProxyConfig(BaseModel): + """Configuration for an ISP proxy.""" + country: Optional[str] = None """ISO 3166 country code. Defaults to US if not provided.""" class ConfigResidentialProxyConfig(BaseModel): + """Configuration for residential proxies.""" + asn: Optional[str] = None """Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html""" @@ -51,6 +57,8 @@ class ConfigResidentialProxyConfig(BaseModel): class ConfigMobileProxyConfig(BaseModel): + """Configuration for mobile proxies.""" + asn: Optional[str] = None """Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html""" @@ -135,6 +143,8 @@ class ConfigMobileProxyConfig(BaseModel): class ConfigCustomProxyConfig(BaseModel): + """Configuration for a custom proxy (e.g., private proxy server).""" + host: str """Proxy host address or IP.""" @@ -158,6 +168,8 @@ class ConfigCustomProxyConfig(BaseModel): class ProxyRetrieveResponse(BaseModel): + """Configuration for routing traffic through a proxy.""" + type: Literal["datacenter", "isp", "residential", "mobile", "custom"] """Proxy type to use. diff --git a/src/kernel/types/shared/app_action.py b/src/kernel/types/shared/app_action.py index 3d711363..1babce1d 100644 --- a/src/kernel/types/shared/app_action.py +++ b/src/kernel/types/shared/app_action.py @@ -6,5 +6,7 @@ class AppAction(BaseModel): + """An action available on the app""" + name: str """Name of the action""" diff --git a/src/kernel/types/shared/browser_extension.py b/src/kernel/types/shared/browser_extension.py index 7bc1a5ff..a91d2dc6 100644 --- a/src/kernel/types/shared/browser_extension.py +++ b/src/kernel/types/shared/browser_extension.py @@ -8,6 +8,11 @@ class BrowserExtension(BaseModel): + """Extension selection for the browser session. + + Provide either id or name of an extension uploaded to Kernel. + """ + id: Optional[str] = None """Extension ID to load for this browser session""" diff --git a/src/kernel/types/shared/browser_profile.py b/src/kernel/types/shared/browser_profile.py index 5f790ccb..4aadc313 100644 --- a/src/kernel/types/shared/browser_profile.py +++ b/src/kernel/types/shared/browser_profile.py @@ -8,6 +8,12 @@ class BrowserProfile(BaseModel): + """Profile selection for the browser session. + + Provide either id or name. If specified, the + matching profile will be loaded into the browser session. Profiles must be created beforehand. + """ + id: Optional[str] = None """Profile ID to load for this browser session""" diff --git a/src/kernel/types/shared/browser_viewport.py b/src/kernel/types/shared/browser_viewport.py index abffcc29..ab8f4273 100644 --- a/src/kernel/types/shared/browser_viewport.py +++ b/src/kernel/types/shared/browser_viewport.py @@ -8,6 +8,15 @@ class BrowserViewport(BaseModel): + """Initial browser window size in pixels with optional refresh rate. + + If omitted, image defaults apply (1920x1080@25). + Only specific viewport configurations are supported. The server will reject unsupported combinations. + Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1024x768@60, 1200x800@60 + If refresh_rate is not provided, it will be automatically determined from the width and height if they match a supported configuration exactly. + Note: Higher resolutions may affect the responsiveness of live view browser + """ + height: int """Browser window height in pixels.""" diff --git a/src/kernel/types/shared/error_event.py b/src/kernel/types/shared/error_event.py index 0041b899..35175f5f 100644 --- a/src/kernel/types/shared/error_event.py +++ b/src/kernel/types/shared/error_event.py @@ -10,6 +10,8 @@ class ErrorEvent(BaseModel): + """An error event from the application.""" + error: ErrorModel event: Literal["error"] diff --git a/src/kernel/types/shared/heartbeat_event.py b/src/kernel/types/shared/heartbeat_event.py index d5ca811f..3745e9b9 100644 --- a/src/kernel/types/shared/heartbeat_event.py +++ b/src/kernel/types/shared/heartbeat_event.py @@ -9,6 +9,8 @@ class HeartbeatEvent(BaseModel): + """Heartbeat event sent periodically to keep SSE connection alive.""" + event: Literal["sse_heartbeat"] """Event type identifier (always "sse_heartbeat").""" diff --git a/src/kernel/types/shared/log_event.py b/src/kernel/types/shared/log_event.py index 69dbc561..078b6eca 100644 --- a/src/kernel/types/shared/log_event.py +++ b/src/kernel/types/shared/log_event.py @@ -9,6 +9,8 @@ class LogEvent(BaseModel): + """A log entry from the application.""" + event: Literal["log"] """Event type identifier (always "log").""" diff --git a/src/kernel/types/shared_params/browser_extension.py b/src/kernel/types/shared_params/browser_extension.py index d81ac708..e6c2b8fa 100644 --- a/src/kernel/types/shared_params/browser_extension.py +++ b/src/kernel/types/shared_params/browser_extension.py @@ -8,6 +8,11 @@ class BrowserExtension(TypedDict, total=False): + """Extension selection for the browser session. + + Provide either id or name of an extension uploaded to Kernel. + """ + id: str """Extension ID to load for this browser session""" diff --git a/src/kernel/types/shared_params/browser_profile.py b/src/kernel/types/shared_params/browser_profile.py index e1027d22..51187dbf 100644 --- a/src/kernel/types/shared_params/browser_profile.py +++ b/src/kernel/types/shared_params/browser_profile.py @@ -8,6 +8,12 @@ class BrowserProfile(TypedDict, total=False): + """Profile selection for the browser session. + + Provide either id or name. If specified, the + matching profile will be loaded into the browser session. Profiles must be created beforehand. + """ + id: str """Profile ID to load for this browser session""" diff --git a/src/kernel/types/shared_params/browser_viewport.py b/src/kernel/types/shared_params/browser_viewport.py index b7cb2f0f..9236547a 100644 --- a/src/kernel/types/shared_params/browser_viewport.py +++ b/src/kernel/types/shared_params/browser_viewport.py @@ -8,6 +8,15 @@ class BrowserViewport(TypedDict, total=False): + """Initial browser window size in pixels with optional refresh rate. + + If omitted, image defaults apply (1920x1080@25). + Only specific viewport configurations are supported. The server will reject unsupported combinations. + Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1024x768@60, 1200x800@60 + If refresh_rate is not provided, it will be automatically determined from the width and height if they match a supported configuration exactly. + Note: Higher resolutions may affect the responsiveness of live view browser + """ + height: Required[int] """Browser window height in pixels.""" From 35ea75430208e75f98f072751241f2778eeb77a0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 19:34:54 +0000 Subject: [PATCH 237/448] feat: Enhance AuthAgent model with last_auth_check_at field --- .stats.yml | 4 ++-- src/kernel/types/agents/auth_agent.py | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 4bf353aa..135345a5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 82 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-b6957db438b01d979b62de21d4e674601b37d55b850b95a6e2b4c771aad5e840.yml -openapi_spec_hash: 1c8aac8322bc9df8f1b82a7e7a0c692b +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-dac11bdb857e700a8c39d183e753ddd1ebaaca69fd9fc5ee57d6b56b70b00e6e.yml +openapi_spec_hash: 78fbc50dd0b61cdc87564fbea278ee23 config_hash: a4b4d14bdf6af723b235a6981977627c diff --git a/src/kernel/types/agents/auth_agent.py b/src/kernel/types/agents/auth_agent.py index ff9f5e9d..423b92e7 100644 --- a/src/kernel/types/agents/auth_agent.py +++ b/src/kernel/types/agents/auth_agent.py @@ -1,5 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from typing import Optional +from datetime import datetime from typing_extensions import Literal from ..._models import BaseModel @@ -23,3 +25,6 @@ class AuthAgent(BaseModel): status: Literal["AUTHENTICATED", "NEEDS_AUTH"] """Current authentication status of the managed profile""" + + last_auth_check_at: Optional[datetime] = None + """When the last authentication check was performed""" From dcd830cfb4392ecb626a8edbd02becbee3dc09a3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 20:19:46 +0000 Subject: [PATCH 238/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index cb9d2541..7f3f5c84 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.22.0" + ".": "0.23.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index facf9996..6e76a589 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.22.0" +version = "0.23.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index c911504a..6e518416 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.22.0" # x-release-please-version +__version__ = "0.23.0" # x-release-please-version From 52e6d0d5d6cc7433dbe768edb6473233123aea3e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 03:33:44 +0000 Subject: [PATCH 239/448] chore(internal): add missing files argument to base client --- src/kernel/_base_client.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/kernel/_base_client.py b/src/kernel/_base_client.py index 756e21e7..e55218be 100644 --- a/src/kernel/_base_client.py +++ b/src/kernel/_base_client.py @@ -1247,9 +1247,12 @@ def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + opts = FinalRequestOptions.construct( + method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + ) return self.request(cast_to, opts) def put( @@ -1767,9 +1770,12 @@ async def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + opts = FinalRequestOptions.construct( + method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + ) return await self.request(cast_to, opts) async def put( From 50e1a883a55ce1246db4d38f8cbb1ac1a13bc5c6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 02:00:41 +0000 Subject: [PATCH 240/448] =?UTF-8?q?feat:=20Enhance=20AuthAgentInvocationCr?= =?UTF-8?q?eateResponse=20to=20include=20already=5Fauthenti=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .stats.yml | 8 +- api.md | 19 + src/kernel/_client.py | 10 +- src/kernel/resources/__init__.py | 14 + src/kernel/resources/agents/auth/auth.py | 189 +++++- .../resources/agents/auth/invocations.py | 60 +- src/kernel/resources/credentials.py | 586 ++++++++++++++++++ src/kernel/types/__init__.py | 4 + src/kernel/types/agents/__init__.py | 1 + .../agents/auth/invocation_create_params.py | 7 + src/kernel/types/agents/auth_agent.py | 17 + .../auth_agent_invocation_create_response.py | 32 +- src/kernel/types/agents/auth_create_params.py | 7 + src/kernel/types/agents/reauth_response.py | 21 + src/kernel/types/credential.py | 26 + src/kernel/types/credential_create_params.py | 19 + src/kernel/types/credential_list_params.py | 18 + src/kernel/types/credential_update_params.py | 19 + .../agents/auth/test_invocations.py | 18 + tests/api_resources/agents/test_auth.py | 172 ++++- tests/api_resources/test_credentials.py | 477 ++++++++++++++ 21 files changed, 1695 insertions(+), 29 deletions(-) create mode 100644 src/kernel/resources/credentials.py create mode 100644 src/kernel/types/agents/reauth_response.py create mode 100644 src/kernel/types/credential.py create mode 100644 src/kernel/types/credential_create_params.py create mode 100644 src/kernel/types/credential_list_params.py create mode 100644 src/kernel/types/credential_update_params.py create mode 100644 tests/api_resources/test_credentials.py diff --git a/.stats.yml b/.stats.yml index 135345a5..0a02606e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 82 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-dac11bdb857e700a8c39d183e753ddd1ebaaca69fd9fc5ee57d6b56b70b00e6e.yml -openapi_spec_hash: 78fbc50dd0b61cdc87564fbea278ee23 -config_hash: a4b4d14bdf6af723b235a6981977627c +configured_endpoints: 89 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-486d57f189abcec3a678ad4a619ee8a6b8aec3a3c2f3620c0423cb16cc755a13.yml +openapi_spec_hash: affde047293fc74a8343a121d5e58a9c +config_hash: 7225e7b7e4695c81d7be26c7108b5494 diff --git a/api.md b/api.md index 2876b33c..d5c3bc62 100644 --- a/api.md +++ b/api.md @@ -297,6 +297,7 @@ from kernel.types.agents import ( AuthAgentInvocationCreateRequest, AuthAgentInvocationCreateResponse, DiscoveredField, + ReauthResponse, ) ``` @@ -305,6 +306,8 @@ Methods: - client.agents.auth.create(\*\*params) -> AuthAgent - client.agents.auth.retrieve(id) -> AuthAgent - client.agents.auth.list(\*\*params) -> SyncOffsetPagination[AuthAgent] +- client.agents.auth.delete(id) -> None +- client.agents.auth.reauth(id) -> ReauthResponse ### Invocations @@ -321,3 +324,19 @@ Methods: - client.agents.auth.invocations.discover(invocation_id, \*\*params) -> AgentAuthDiscoverResponse - client.agents.auth.invocations.exchange(invocation_id, \*\*params) -> InvocationExchangeResponse - client.agents.auth.invocations.submit(invocation_id, \*\*params) -> AgentAuthSubmitResponse + +# Credentials + +Types: + +```python +from kernel.types import CreateCredentialRequest, Credential, UpdateCredentialRequest +``` + +Methods: + +- client.credentials.create(\*\*params) -> Credential +- client.credentials.retrieve(id) -> Credential +- client.credentials.update(id, \*\*params) -> Credential +- client.credentials.list(\*\*params) -> SyncOffsetPagination[Credential] +- client.credentials.delete(id) -> None diff --git a/src/kernel/_client.py b/src/kernel/_client.py index c941be79..4dc11976 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -21,7 +21,7 @@ ) from ._utils import is_given, get_async_library from ._version import __version__ -from .resources import apps, proxies, profiles, extensions, deployments, invocations, browser_pools +from .resources import apps, proxies, profiles, extensions, credentials, deployments, invocations, browser_pools from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import KernelError, APIStatusError from ._base_client import ( @@ -60,6 +60,7 @@ class Kernel(SyncAPIClient): extensions: extensions.ExtensionsResource browser_pools: browser_pools.BrowserPoolsResource agents: agents.AgentsResource + credentials: credentials.CredentialsResource with_raw_response: KernelWithRawResponse with_streaming_response: KernelWithStreamedResponse @@ -150,6 +151,7 @@ def __init__( self.extensions = extensions.ExtensionsResource(self) self.browser_pools = browser_pools.BrowserPoolsResource(self) self.agents = agents.AgentsResource(self) + self.credentials = credentials.CredentialsResource(self) self.with_raw_response = KernelWithRawResponse(self) self.with_streaming_response = KernelWithStreamedResponse(self) @@ -270,6 +272,7 @@ class AsyncKernel(AsyncAPIClient): extensions: extensions.AsyncExtensionsResource browser_pools: browser_pools.AsyncBrowserPoolsResource agents: agents.AsyncAgentsResource + credentials: credentials.AsyncCredentialsResource with_raw_response: AsyncKernelWithRawResponse with_streaming_response: AsyncKernelWithStreamedResponse @@ -360,6 +363,7 @@ def __init__( self.extensions = extensions.AsyncExtensionsResource(self) self.browser_pools = browser_pools.AsyncBrowserPoolsResource(self) self.agents = agents.AsyncAgentsResource(self) + self.credentials = credentials.AsyncCredentialsResource(self) self.with_raw_response = AsyncKernelWithRawResponse(self) self.with_streaming_response = AsyncKernelWithStreamedResponse(self) @@ -481,6 +485,7 @@ def __init__(self, client: Kernel) -> None: self.extensions = extensions.ExtensionsResourceWithRawResponse(client.extensions) self.browser_pools = browser_pools.BrowserPoolsResourceWithRawResponse(client.browser_pools) self.agents = agents.AgentsResourceWithRawResponse(client.agents) + self.credentials = credentials.CredentialsResourceWithRawResponse(client.credentials) class AsyncKernelWithRawResponse: @@ -494,6 +499,7 @@ def __init__(self, client: AsyncKernel) -> None: self.extensions = extensions.AsyncExtensionsResourceWithRawResponse(client.extensions) self.browser_pools = browser_pools.AsyncBrowserPoolsResourceWithRawResponse(client.browser_pools) self.agents = agents.AsyncAgentsResourceWithRawResponse(client.agents) + self.credentials = credentials.AsyncCredentialsResourceWithRawResponse(client.credentials) class KernelWithStreamedResponse: @@ -507,6 +513,7 @@ def __init__(self, client: Kernel) -> None: self.extensions = extensions.ExtensionsResourceWithStreamingResponse(client.extensions) self.browser_pools = browser_pools.BrowserPoolsResourceWithStreamingResponse(client.browser_pools) self.agents = agents.AgentsResourceWithStreamingResponse(client.agents) + self.credentials = credentials.CredentialsResourceWithStreamingResponse(client.credentials) class AsyncKernelWithStreamedResponse: @@ -520,6 +527,7 @@ def __init__(self, client: AsyncKernel) -> None: self.extensions = extensions.AsyncExtensionsResourceWithStreamingResponse(client.extensions) self.browser_pools = browser_pools.AsyncBrowserPoolsResourceWithStreamingResponse(client.browser_pools) self.agents = agents.AsyncAgentsResourceWithStreamingResponse(client.agents) + self.credentials = credentials.AsyncCredentialsResourceWithStreamingResponse(client.credentials) Client = Kernel diff --git a/src/kernel/resources/__init__.py b/src/kernel/resources/__init__.py index 5de2a858..e6e81036 100644 --- a/src/kernel/resources/__init__.py +++ b/src/kernel/resources/__init__.py @@ -48,6 +48,14 @@ ExtensionsResourceWithStreamingResponse, AsyncExtensionsResourceWithStreamingResponse, ) +from .credentials import ( + CredentialsResource, + AsyncCredentialsResource, + CredentialsResourceWithRawResponse, + AsyncCredentialsResourceWithRawResponse, + CredentialsResourceWithStreamingResponse, + AsyncCredentialsResourceWithStreamingResponse, +) from .deployments import ( DeploymentsResource, AsyncDeploymentsResource, @@ -128,4 +136,10 @@ "AsyncAgentsResourceWithRawResponse", "AgentsResourceWithStreamingResponse", "AsyncAgentsResourceWithStreamingResponse", + "CredentialsResource", + "AsyncCredentialsResource", + "CredentialsResourceWithRawResponse", + "AsyncCredentialsResourceWithRawResponse", + "CredentialsResourceWithStreamingResponse", + "AsyncCredentialsResourceWithStreamingResponse", ] diff --git a/src/kernel/resources/agents/auth/auth.py b/src/kernel/resources/agents/auth/auth.py index b4fc7588..39f22fcb 100644 --- a/src/kernel/resources/agents/auth/auth.py +++ b/src/kernel/resources/agents/auth/auth.py @@ -4,7 +4,7 @@ import httpx -from ...._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from ...._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given from ...._utils import maybe_transform, async_maybe_transform from ...._compat import cached_property from .invocations import ( @@ -26,6 +26,7 @@ from ...._base_client import AsyncPaginator, make_request_options from ....types.agents import auth_list_params, auth_create_params from ....types.agents.auth_agent import AuthAgent +from ....types.agents.reauth_response import ReauthResponse __all__ = ["AuthResource", "AsyncAuthResource"] @@ -59,6 +60,7 @@ def create( *, profile_name: str, target_domain: str, + credential_name: str | Omit = omit, login_url: str | Omit = omit, proxy: auth_create_params.Proxy | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -79,6 +81,10 @@ def create( target_domain: Target domain for authentication + credential_name: Optional name of an existing credential to use for this auth agent. If provided, + the credential will be linked to the agent and its values will be used to + auto-fill the login form on invocation. + login_url: Optional login page URL. If provided, will be stored on the agent and used to skip discovery in future invocations. @@ -98,6 +104,7 @@ def create( { "profile_name": profile_name, "target_domain": target_domain, + "credential_name": credential_name, "login_url": login_url, "proxy": proxy, }, @@ -199,6 +206,81 @@ def list( model=AuthAgent, ) + def delete( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """Deletes an auth agent and terminates its workflow. + + This will: + + - Soft delete the auth agent record + - Gracefully terminate the agent's Temporal workflow + - Cancel any in-progress invocations + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + f"/agents/auth/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + def reauth( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ReauthResponse: + """ + Triggers automatic re-authentication for an auth agent using stored credentials. + Requires the auth agent to have a linked credential, stored selectors, and + login_url. Returns immediately with status indicating whether re-auth was + started. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/agents/auth/{id}/reauth", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ReauthResponse, + ) + class AsyncAuthResource(AsyncAPIResource): @cached_property @@ -229,6 +311,7 @@ async def create( *, profile_name: str, target_domain: str, + credential_name: str | Omit = omit, login_url: str | Omit = omit, proxy: auth_create_params.Proxy | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -249,6 +332,10 @@ async def create( target_domain: Target domain for authentication + credential_name: Optional name of an existing credential to use for this auth agent. If provided, + the credential will be linked to the agent and its values will be used to + auto-fill the login form on invocation. + login_url: Optional login page URL. If provided, will be stored on the agent and used to skip discovery in future invocations. @@ -268,6 +355,7 @@ async def create( { "profile_name": profile_name, "target_domain": target_domain, + "credential_name": credential_name, "login_url": login_url, "proxy": proxy, }, @@ -369,6 +457,81 @@ def list( model=AuthAgent, ) + async def delete( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """Deletes an auth agent and terminates its workflow. + + This will: + + - Soft delete the auth agent record + - Gracefully terminate the agent's Temporal workflow + - Cancel any in-progress invocations + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + f"/agents/auth/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + async def reauth( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ReauthResponse: + """ + Triggers automatic re-authentication for an auth agent using stored credentials. + Requires the auth agent to have a linked credential, stored selectors, and + login_url. Returns immediately with status indicating whether re-auth was + started. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/agents/auth/{id}/reauth", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ReauthResponse, + ) + class AuthResourceWithRawResponse: def __init__(self, auth: AuthResource) -> None: @@ -383,6 +546,12 @@ def __init__(self, auth: AuthResource) -> None: self.list = to_raw_response_wrapper( auth.list, ) + self.delete = to_raw_response_wrapper( + auth.delete, + ) + self.reauth = to_raw_response_wrapper( + auth.reauth, + ) @cached_property def invocations(self) -> InvocationsResourceWithRawResponse: @@ -402,6 +571,12 @@ def __init__(self, auth: AsyncAuthResource) -> None: self.list = async_to_raw_response_wrapper( auth.list, ) + self.delete = async_to_raw_response_wrapper( + auth.delete, + ) + self.reauth = async_to_raw_response_wrapper( + auth.reauth, + ) @cached_property def invocations(self) -> AsyncInvocationsResourceWithRawResponse: @@ -421,6 +596,12 @@ def __init__(self, auth: AuthResource) -> None: self.list = to_streamed_response_wrapper( auth.list, ) + self.delete = to_streamed_response_wrapper( + auth.delete, + ) + self.reauth = to_streamed_response_wrapper( + auth.reauth, + ) @cached_property def invocations(self) -> InvocationsResourceWithStreamingResponse: @@ -440,6 +621,12 @@ def __init__(self, auth: AsyncAuthResource) -> None: self.list = async_to_streamed_response_wrapper( auth.list, ) + self.delete = async_to_streamed_response_wrapper( + auth.delete, + ) + self.reauth = async_to_streamed_response_wrapper( + auth.reauth, + ) @cached_property def invocations(self) -> AsyncInvocationsResourceWithStreamingResponse: diff --git a/src/kernel/resources/agents/auth/invocations.py b/src/kernel/resources/agents/auth/invocations.py index 361f4242..ac2bb8ed 100644 --- a/src/kernel/resources/agents/auth/invocations.py +++ b/src/kernel/resources/agents/auth/invocations.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Dict +from typing import Any, Dict, cast import httpx @@ -56,6 +56,7 @@ def create( self, *, auth_agent_id: str, + save_credential_as: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -72,6 +73,10 @@ def create( Args: auth_agent_id: ID of the auth agent to create an invocation for + save_credential_as: If provided, saves the submitted credentials under this name upon successful + login. The credential will be linked to the auth agent for automatic + re-authentication. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -80,13 +85,24 @@ def create( timeout: Override the client-level default timeout for this request, in seconds """ - return self._post( - "/agents/auth/invocations", - body=maybe_transform({"auth_agent_id": auth_agent_id}, invocation_create_params.InvocationCreateParams), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + return cast( + AuthAgentInvocationCreateResponse, + self._post( + "/agents/auth/invocations", + body=maybe_transform( + { + "auth_agent_id": auth_agent_id, + "save_credential_as": save_credential_as, + }, + invocation_create_params.InvocationCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=cast( + Any, AuthAgentInvocationCreateResponse + ), # Union types cannot be passed in as arguments in the type system ), - cast_to=AuthAgentInvocationCreateResponse, ) def retrieve( @@ -266,6 +282,7 @@ async def create( self, *, auth_agent_id: str, + save_credential_as: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -282,6 +299,10 @@ async def create( Args: auth_agent_id: ID of the auth agent to create an invocation for + save_credential_as: If provided, saves the submitted credentials under this name upon successful + login. The credential will be linked to the auth agent for automatic + re-authentication. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -290,15 +311,24 @@ async def create( timeout: Override the client-level default timeout for this request, in seconds """ - return await self._post( - "/agents/auth/invocations", - body=await async_maybe_transform( - {"auth_agent_id": auth_agent_id}, invocation_create_params.InvocationCreateParams - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + return cast( + AuthAgentInvocationCreateResponse, + await self._post( + "/agents/auth/invocations", + body=await async_maybe_transform( + { + "auth_agent_id": auth_agent_id, + "save_credential_as": save_credential_as, + }, + invocation_create_params.InvocationCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=cast( + Any, AuthAgentInvocationCreateResponse + ), # Union types cannot be passed in as arguments in the type system ), - cast_to=AuthAgentInvocationCreateResponse, ) async def retrieve( diff --git a/src/kernel/resources/credentials.py b/src/kernel/resources/credentials.py new file mode 100644 index 00000000..91dafce8 --- /dev/null +++ b/src/kernel/resources/credentials.py @@ -0,0 +1,586 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict + +import httpx + +from ..types import credential_list_params, credential_create_params, credential_update_params +from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given +from .._utils import maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..pagination import SyncOffsetPagination, AsyncOffsetPagination +from .._base_client import AsyncPaginator, make_request_options +from ..types.credential import Credential + +__all__ = ["CredentialsResource", "AsyncCredentialsResource"] + + +class CredentialsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> CredentialsResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return CredentialsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> CredentialsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return CredentialsResourceWithStreamingResponse(self) + + def create( + self, + *, + domain: str, + name: str, + values: Dict[str, 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, + ) -> Credential: + """Create a new credential for storing login information. + + Values are encrypted at + rest. + + Args: + domain: Target domain this credential is for + + name: Unique name for the credential within the organization + + values: Field name to value mapping (e.g., username, password) + + 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( + "/credentials", + body=maybe_transform( + { + "domain": domain, + "name": name, + "values": values, + }, + credential_create_params.CredentialCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Credential, + ) + + def retrieve( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Credential: + """Retrieve a credential by its ID. + + Credential values are not returned. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/credentials/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Credential, + ) + + def update( + self, + id: str, + *, + name: str | Omit = omit, + values: Dict[str, str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Credential: + """Update a credential's name or values. + + Values are encrypted at rest. + + Args: + name: New name for the credential + + values: Field name to value mapping (e.g., username, password). Replaces all existing + values. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._patch( + f"/credentials/{id}", + body=maybe_transform( + { + "name": name, + "values": values, + }, + credential_update_params.CredentialUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Credential, + ) + + def list( + self, + *, + domain: str | Omit = omit, + limit: int | Omit = omit, + offset: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SyncOffsetPagination[Credential]: + """List credentials owned by the caller's organization. + + Credential values are not + returned. + + Args: + domain: Filter by domain + + limit: Maximum number of results to return + + offset: Number of results to skip + + 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_api_list( + "/credentials", + page=SyncOffsetPagination[Credential], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "domain": domain, + "limit": limit, + "offset": offset, + }, + credential_list_params.CredentialListParams, + ), + ), + model=Credential, + ) + + def delete( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + Delete a credential by its ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + f"/credentials/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class AsyncCredentialsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncCredentialsResourceWithRawResponse: + """ + 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncCredentialsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncCredentialsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + """ + return AsyncCredentialsResourceWithStreamingResponse(self) + + async def create( + self, + *, + domain: str, + name: str, + values: Dict[str, 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, + ) -> Credential: + """Create a new credential for storing login information. + + Values are encrypted at + rest. + + Args: + domain: Target domain this credential is for + + name: Unique name for the credential within the organization + + values: Field name to value mapping (e.g., username, password) + + 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( + "/credentials", + body=await async_maybe_transform( + { + "domain": domain, + "name": name, + "values": values, + }, + credential_create_params.CredentialCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Credential, + ) + + async def retrieve( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Credential: + """Retrieve a credential by its ID. + + Credential values are not returned. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/credentials/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Credential, + ) + + async def update( + self, + id: str, + *, + name: str | Omit = omit, + values: Dict[str, str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Credential: + """Update a credential's name or values. + + Values are encrypted at rest. + + Args: + name: New name for the credential + + values: Field name to value mapping (e.g., username, password). Replaces all existing + values. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._patch( + f"/credentials/{id}", + body=await async_maybe_transform( + { + "name": name, + "values": values, + }, + credential_update_params.CredentialUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Credential, + ) + + def list( + self, + *, + domain: str | Omit = omit, + limit: int | Omit = omit, + offset: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[Credential, AsyncOffsetPagination[Credential]]: + """List credentials owned by the caller's organization. + + Credential values are not + returned. + + Args: + domain: Filter by domain + + limit: Maximum number of results to return + + offset: Number of results to skip + + 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_api_list( + "/credentials", + page=AsyncOffsetPagination[Credential], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "domain": domain, + "limit": limit, + "offset": offset, + }, + credential_list_params.CredentialListParams, + ), + ), + model=Credential, + ) + + async def delete( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + Delete a credential by its ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + f"/credentials/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class CredentialsResourceWithRawResponse: + def __init__(self, credentials: CredentialsResource) -> None: + self._credentials = credentials + + self.create = to_raw_response_wrapper( + credentials.create, + ) + self.retrieve = to_raw_response_wrapper( + credentials.retrieve, + ) + self.update = to_raw_response_wrapper( + credentials.update, + ) + self.list = to_raw_response_wrapper( + credentials.list, + ) + self.delete = to_raw_response_wrapper( + credentials.delete, + ) + + +class AsyncCredentialsResourceWithRawResponse: + def __init__(self, credentials: AsyncCredentialsResource) -> None: + self._credentials = credentials + + self.create = async_to_raw_response_wrapper( + credentials.create, + ) + self.retrieve = async_to_raw_response_wrapper( + credentials.retrieve, + ) + self.update = async_to_raw_response_wrapper( + credentials.update, + ) + self.list = async_to_raw_response_wrapper( + credentials.list, + ) + self.delete = async_to_raw_response_wrapper( + credentials.delete, + ) + + +class CredentialsResourceWithStreamingResponse: + def __init__(self, credentials: CredentialsResource) -> None: + self._credentials = credentials + + self.create = to_streamed_response_wrapper( + credentials.create, + ) + self.retrieve = to_streamed_response_wrapper( + credentials.retrieve, + ) + self.update = to_streamed_response_wrapper( + credentials.update, + ) + self.list = to_streamed_response_wrapper( + credentials.list, + ) + self.delete = to_streamed_response_wrapper( + credentials.delete, + ) + + +class AsyncCredentialsResourceWithStreamingResponse: + def __init__(self, credentials: AsyncCredentialsResource) -> None: + self._credentials = credentials + + self.create = async_to_streamed_response_wrapper( + credentials.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + credentials.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + credentials.update, + ) + self.list = async_to_streamed_response_wrapper( + credentials.list, + ) + self.delete = async_to_streamed_response_wrapper( + credentials.delete, + ) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 45b0c4ba..b5a9804e 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -14,6 +14,7 @@ BrowserExtension as BrowserExtension, ) from .profile import Profile as Profile +from .credential import Credential as Credential from .browser_pool import BrowserPool as BrowserPool from .app_list_params import AppListParams as AppListParams from .app_list_response import AppListResponse as AppListResponse @@ -28,6 +29,7 @@ from .profile_create_params import ProfileCreateParams as ProfileCreateParams from .profile_list_response import ProfileListResponse as ProfileListResponse from .proxy_create_response import ProxyCreateResponse as ProxyCreateResponse +from .credential_list_params import CredentialListParams as CredentialListParams from .deployment_list_params import DeploymentListParams as DeploymentListParams from .deployment_state_event import DeploymentStateEvent as DeploymentStateEvent from .invocation_list_params import InvocationListParams as InvocationListParams @@ -36,6 +38,8 @@ from .extension_list_response import ExtensionListResponse as ExtensionListResponse from .extension_upload_params import ExtensionUploadParams as ExtensionUploadParams from .proxy_retrieve_response import ProxyRetrieveResponse as ProxyRetrieveResponse +from .credential_create_params import CredentialCreateParams as CredentialCreateParams +from .credential_update_params import CredentialUpdateParams as CredentialUpdateParams from .deployment_create_params import DeploymentCreateParams as DeploymentCreateParams from .deployment_follow_params import DeploymentFollowParams as DeploymentFollowParams from .deployment_list_response import DeploymentListResponse as DeploymentListResponse diff --git a/src/kernel/types/agents/__init__.py b/src/kernel/types/agents/__init__.py index 686e805a..e2c3bb0c 100644 --- a/src/kernel/types/agents/__init__.py +++ b/src/kernel/types/agents/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations from .auth_agent import AuthAgent as AuthAgent +from .reauth_response import ReauthResponse as ReauthResponse from .auth_list_params import AuthListParams as AuthListParams from .discovered_field import DiscoveredField as DiscoveredField from .auth_create_params import AuthCreateParams as AuthCreateParams diff --git a/src/kernel/types/agents/auth/invocation_create_params.py b/src/kernel/types/agents/auth/invocation_create_params.py index b3de645e..b2727e02 100644 --- a/src/kernel/types/agents/auth/invocation_create_params.py +++ b/src/kernel/types/agents/auth/invocation_create_params.py @@ -10,3 +10,10 @@ class InvocationCreateParams(TypedDict, total=False): auth_agent_id: Required[str] """ID of the auth agent to create an invocation for""" + + save_credential_as: str + """ + If provided, saves the submitted credentials under this name upon successful + login. The credential will be linked to the auth agent for automatic + re-authentication. + """ diff --git a/src/kernel/types/agents/auth_agent.py b/src/kernel/types/agents/auth_agent.py index 423b92e7..50f91499 100644 --- a/src/kernel/types/agents/auth_agent.py +++ b/src/kernel/types/agents/auth_agent.py @@ -26,5 +26,22 @@ class AuthAgent(BaseModel): status: Literal["AUTHENTICATED", "NEEDS_AUTH"] """Current authentication status of the managed profile""" + can_reauth: Optional[bool] = None + """ + Whether automatic re-authentication is possible (has credential_id, selectors, + and login_url) + """ + + credential_id: Optional[str] = None + """ID of the linked credential for automatic re-authentication""" + + credential_name: Optional[str] = None + """Name of the linked credential for automatic re-authentication""" + + has_selectors: Optional[bool] = None + """ + Whether this auth agent has stored selectors for deterministic re-authentication + """ + last_auth_check_at: Optional[datetime] = None """When the last authentication check was performed""" diff --git a/src/kernel/types/agents/auth_agent_invocation_create_response.py b/src/kernel/types/agents/auth_agent_invocation_create_response.py index baa80e2a..da0b6f64 100644 --- a/src/kernel/types/agents/auth_agent_invocation_create_response.py +++ b/src/kernel/types/agents/auth_agent_invocation_create_response.py @@ -1,23 +1,41 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from typing import Union from datetime import datetime +from typing_extensions import Literal, Annotated, TypeAlias +from ..._utils import PropertyInfo from ..._models import BaseModel -__all__ = ["AuthAgentInvocationCreateResponse"] +__all__ = ["AuthAgentInvocationCreateResponse", "AuthAgentAlreadyAuthenticated", "AuthAgentInvocationCreated"] -class AuthAgentInvocationCreateResponse(BaseModel): - """Response from creating an auth agent invocation""" +class AuthAgentAlreadyAuthenticated(BaseModel): + """Response when the agent is already authenticated.""" + + status: Literal["already_authenticated"] + """Indicates the agent is already authenticated and no invocation was created.""" + + +class AuthAgentInvocationCreated(BaseModel): + """Response when a new invocation was created.""" expires_at: datetime - """When the handoff code expires""" + """When the handoff code expires.""" handoff_code: str - """One-time code for handoff""" + """One-time code for handoff.""" hosted_url: str - """URL to redirect user to""" + """URL to redirect user to.""" invocation_id: str - """Unique identifier for the invocation""" + """Unique identifier for the invocation.""" + + status: Literal["invocation_created"] + """Indicates an invocation was created.""" + + +AuthAgentInvocationCreateResponse: TypeAlias = Annotated[ + Union[AuthAgentAlreadyAuthenticated, AuthAgentInvocationCreated], PropertyInfo(discriminator="status") +] diff --git a/src/kernel/types/agents/auth_create_params.py b/src/kernel/types/agents/auth_create_params.py index 7869925e..7cf7665f 100644 --- a/src/kernel/types/agents/auth_create_params.py +++ b/src/kernel/types/agents/auth_create_params.py @@ -14,6 +14,13 @@ class AuthCreateParams(TypedDict, total=False): target_domain: Required[str] """Target domain for authentication""" + credential_name: str + """Optional name of an existing credential to use for this auth agent. + + If provided, the credential will be linked to the agent and its values will be + used to auto-fill the login form on invocation. + """ + login_url: str """Optional login page URL. diff --git a/src/kernel/types/agents/reauth_response.py b/src/kernel/types/agents/reauth_response.py new file mode 100644 index 00000000..4ead46a4 --- /dev/null +++ b/src/kernel/types/agents/reauth_response.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["ReauthResponse"] + + +class ReauthResponse(BaseModel): + """Response from triggering re-authentication""" + + status: Literal["reauth_started", "already_authenticated", "cannot_reauth"] + """Result of the re-authentication attempt""" + + invocation_id: Optional[str] = None + """ID of the re-auth invocation if one was created""" + + message: Optional[str] = None + """Human-readable description of the result""" diff --git a/src/kernel/types/credential.py b/src/kernel/types/credential.py new file mode 100644 index 00000000..30def062 --- /dev/null +++ b/src/kernel/types/credential.py @@ -0,0 +1,26 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from datetime import datetime + +from .._models import BaseModel + +__all__ = ["Credential"] + + +class Credential(BaseModel): + """A stored credential for automatic re-authentication""" + + id: str + """Unique identifier for the credential""" + + created_at: datetime + """When the credential was created""" + + domain: str + """Target domain this credential is for""" + + name: str + """Unique name for the credential within the organization""" + + updated_at: datetime + """When the credential was last updated""" diff --git a/src/kernel/types/credential_create_params.py b/src/kernel/types/credential_create_params.py new file mode 100644 index 00000000..3f7b2d90 --- /dev/null +++ b/src/kernel/types/credential_create_params.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict +from typing_extensions import Required, TypedDict + +__all__ = ["CredentialCreateParams"] + + +class CredentialCreateParams(TypedDict, total=False): + domain: Required[str] + """Target domain this credential is for""" + + name: Required[str] + """Unique name for the credential within the organization""" + + values: Required[Dict[str, str]] + """Field name to value mapping (e.g., username, password)""" diff --git a/src/kernel/types/credential_list_params.py b/src/kernel/types/credential_list_params.py new file mode 100644 index 00000000..945909e5 --- /dev/null +++ b/src/kernel/types/credential_list_params.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["CredentialListParams"] + + +class CredentialListParams(TypedDict, total=False): + domain: str + """Filter by domain""" + + limit: int + """Maximum number of results to return""" + + offset: int + """Number of results to skip""" diff --git a/src/kernel/types/credential_update_params.py b/src/kernel/types/credential_update_params.py new file mode 100644 index 00000000..ffc0c1cc --- /dev/null +++ b/src/kernel/types/credential_update_params.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict +from typing_extensions import TypedDict + +__all__ = ["CredentialUpdateParams"] + + +class CredentialUpdateParams(TypedDict, total=False): + name: str + """New name for the credential""" + + values: Dict[str, str] + """Field name to value mapping (e.g., username, password). + + Replaces all existing values. + """ diff --git a/tests/api_resources/agents/auth/test_invocations.py b/tests/api_resources/agents/auth/test_invocations.py index 7957caf5..eef21a9b 100644 --- a/tests/api_resources/agents/auth/test_invocations.py +++ b/tests/api_resources/agents/auth/test_invocations.py @@ -33,6 +33,15 @@ def test_method_create(self, client: Kernel) -> None: ) assert_matches_type(AuthAgentInvocationCreateResponse, invocation, path=["response"]) + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_create_with_all_params(self, client: Kernel) -> None: + invocation = client.agents.auth.invocations.create( + auth_agent_id="abc123xyz", + save_credential_as="my-netflix-login", + ) + assert_matches_type(AuthAgentInvocationCreateResponse, invocation, path=["response"]) + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_create(self, client: Kernel) -> None: @@ -270,6 +279,15 @@ async def test_method_create(self, async_client: AsyncKernel) -> None: ) assert_matches_type(AuthAgentInvocationCreateResponse, invocation, path=["response"]) + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: + invocation = await async_client.agents.auth.invocations.create( + auth_agent_id="abc123xyz", + save_credential_as="my-netflix-login", + ) + assert_matches_type(AuthAgentInvocationCreateResponse, invocation, path=["response"]) + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_create(self, async_client: AsyncKernel) -> None: diff --git a/tests/api_resources/agents/test_auth.py b/tests/api_resources/agents/test_auth.py index 5115f901..192361ad 100644 --- a/tests/api_resources/agents/test_auth.py +++ b/tests/api_resources/agents/test_auth.py @@ -10,7 +10,7 @@ from kernel import Kernel, AsyncKernel from tests.utils import assert_matches_type from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination -from kernel.types.agents import AuthAgent +from kernel.types.agents import AuthAgent, ReauthResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -33,6 +33,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: auth = client.agents.auth.create( profile_name="user-123", target_domain="netflix.com", + credential_name="my-netflix-login", login_url="https://netflix.com/login", proxy={"proxy_id": "proxy_id"}, ) @@ -147,6 +148,90 @@ def test_streaming_response_list(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_delete(self, client: Kernel) -> None: + auth = client.agents.auth.delete( + "id", + ) + assert auth is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_delete(self, client: Kernel) -> None: + response = client.agents.auth.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + auth = response.parse() + assert auth is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_delete(self, client: Kernel) -> None: + with client.agents.auth.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + auth = response.parse() + assert auth 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: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.agents.auth.with_raw_response.delete( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_reauth(self, client: Kernel) -> None: + auth = client.agents.auth.reauth( + "id", + ) + assert_matches_type(ReauthResponse, auth, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_reauth(self, client: Kernel) -> None: + response = client.agents.auth.with_raw_response.reauth( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + auth = response.parse() + assert_matches_type(ReauthResponse, auth, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_reauth(self, client: Kernel) -> None: + with client.agents.auth.with_streaming_response.reauth( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + auth = response.parse() + assert_matches_type(ReauthResponse, auth, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_reauth(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.agents.auth.with_raw_response.reauth( + "", + ) + class TestAsyncAuth: parametrize = pytest.mark.parametrize( @@ -168,6 +253,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> auth = await async_client.agents.auth.create( profile_name="user-123", target_domain="netflix.com", + credential_name="my-netflix-login", login_url="https://netflix.com/login", proxy={"proxy_id": "proxy_id"}, ) @@ -281,3 +367,87 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: assert_matches_type(AsyncOffsetPagination[AuthAgent], auth, 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: AsyncKernel) -> None: + auth = await async_client.agents.auth.delete( + "id", + ) + assert auth is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: + response = await async_client.agents.auth.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + auth = await response.parse() + assert auth is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None: + async with async_client.agents.auth.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + auth = await response.parse() + assert auth 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: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.agents.auth.with_raw_response.delete( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_reauth(self, async_client: AsyncKernel) -> None: + auth = await async_client.agents.auth.reauth( + "id", + ) + assert_matches_type(ReauthResponse, auth, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_reauth(self, async_client: AsyncKernel) -> None: + response = await async_client.agents.auth.with_raw_response.reauth( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + auth = await response.parse() + assert_matches_type(ReauthResponse, auth, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_reauth(self, async_client: AsyncKernel) -> None: + async with async_client.agents.auth.with_streaming_response.reauth( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + auth = await response.parse() + assert_matches_type(ReauthResponse, auth, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_reauth(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.agents.auth.with_raw_response.reauth( + "", + ) diff --git a/tests/api_resources/test_credentials.py b/tests/api_resources/test_credentials.py new file mode 100644 index 00000000..00c7635e --- /dev/null +++ b/tests/api_resources/test_credentials.py @@ -0,0 +1,477 @@ +# 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 kernel import Kernel, AsyncKernel +from tests.utils import assert_matches_type +from kernel.types import Credential +from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestCredentials: + 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: Kernel) -> None: + credential = client.credentials.create( + domain="netflix.com", + name="my-netflix-login", + values={ + "username": "user@example.com", + "password": "mysecretpassword", + }, + ) + assert_matches_type(Credential, credential, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_create(self, client: Kernel) -> None: + response = client.credentials.with_raw_response.create( + domain="netflix.com", + name="my-netflix-login", + values={ + "username": "user@example.com", + "password": "mysecretpassword", + }, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + credential = response.parse() + assert_matches_type(Credential, credential, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_create(self, client: Kernel) -> None: + with client.credentials.with_streaming_response.create( + domain="netflix.com", + name="my-netflix-login", + values={ + "username": "user@example.com", + "password": "mysecretpassword", + }, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + credential = response.parse() + assert_matches_type(Credential, credential, 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: Kernel) -> None: + credential = client.credentials.retrieve( + "id", + ) + assert_matches_type(Credential, credential, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: Kernel) -> None: + response = client.credentials.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + credential = response.parse() + assert_matches_type(Credential, credential, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: Kernel) -> None: + with client.credentials.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + credential = response.parse() + assert_matches_type(Credential, credential, 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: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.credentials.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_update(self, client: Kernel) -> None: + credential = client.credentials.update( + id="id", + ) + assert_matches_type(Credential, credential, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_update_with_all_params(self, client: Kernel) -> None: + credential = client.credentials.update( + id="id", + name="my-updated-login", + values={ + "username": "user@example.com", + "password": "newpassword", + }, + ) + assert_matches_type(Credential, credential, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_update(self, client: Kernel) -> None: + response = client.credentials.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + credential = response.parse() + assert_matches_type(Credential, credential, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_update(self, client: Kernel) -> None: + with client.credentials.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + credential = response.parse() + assert_matches_type(Credential, credential, 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: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.credentials.with_raw_response.update( + id="", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_list(self, client: Kernel) -> None: + credential = client.credentials.list() + assert_matches_type(SyncOffsetPagination[Credential], credential, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_list_with_all_params(self, client: Kernel) -> None: + credential = client.credentials.list( + domain="domain", + limit=100, + offset=0, + ) + assert_matches_type(SyncOffsetPagination[Credential], credential, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_list(self, client: Kernel) -> None: + response = client.credentials.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + credential = response.parse() + assert_matches_type(SyncOffsetPagination[Credential], credential, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_list(self, client: Kernel) -> None: + with client.credentials.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + credential = response.parse() + assert_matches_type(SyncOffsetPagination[Credential], credential, 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: Kernel) -> None: + credential = client.credentials.delete( + "id", + ) + assert credential is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_delete(self, client: Kernel) -> None: + response = client.credentials.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + credential = response.parse() + assert credential is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_delete(self, client: Kernel) -> None: + with client.credentials.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + credential = response.parse() + assert credential 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: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.credentials.with_raw_response.delete( + "", + ) + + +class TestAsyncCredentials: + 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: AsyncKernel) -> None: + credential = await async_client.credentials.create( + domain="netflix.com", + name="my-netflix-login", + values={ + "username": "user@example.com", + "password": "mysecretpassword", + }, + ) + assert_matches_type(Credential, credential, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_create(self, async_client: AsyncKernel) -> None: + response = await async_client.credentials.with_raw_response.create( + domain="netflix.com", + name="my-netflix-login", + values={ + "username": "user@example.com", + "password": "mysecretpassword", + }, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + credential = await response.parse() + assert_matches_type(Credential, credential, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: + async with async_client.credentials.with_streaming_response.create( + domain="netflix.com", + name="my-netflix-login", + values={ + "username": "user@example.com", + "password": "mysecretpassword", + }, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + credential = await response.parse() + assert_matches_type(Credential, credential, 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: AsyncKernel) -> None: + credential = await async_client.credentials.retrieve( + "id", + ) + assert_matches_type(Credential, credential, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: + response = await async_client.credentials.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + credential = await response.parse() + assert_matches_type(Credential, credential, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: + async with async_client.credentials.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + credential = await response.parse() + assert_matches_type(Credential, credential, 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: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.credentials.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_update(self, async_client: AsyncKernel) -> None: + credential = await async_client.credentials.update( + id="id", + ) + assert_matches_type(Credential, credential, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> None: + credential = await async_client.credentials.update( + id="id", + name="my-updated-login", + values={ + "username": "user@example.com", + "password": "newpassword", + }, + ) + assert_matches_type(Credential, credential, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_update(self, async_client: AsyncKernel) -> None: + response = await async_client.credentials.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + credential = await response.parse() + assert_matches_type(Credential, credential, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_update(self, async_client: AsyncKernel) -> None: + async with async_client.credentials.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + credential = await response.parse() + assert_matches_type(Credential, credential, 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: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.credentials.with_raw_response.update( + id="", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncKernel) -> None: + credential = await async_client.credentials.list() + assert_matches_type(AsyncOffsetPagination[Credential], credential, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: + credential = await async_client.credentials.list( + domain="domain", + limit=100, + offset=0, + ) + assert_matches_type(AsyncOffsetPagination[Credential], credential, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncKernel) -> None: + response = await async_client.credentials.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + credential = await response.parse() + assert_matches_type(AsyncOffsetPagination[Credential], credential, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: + async with async_client.credentials.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + credential = await response.parse() + assert_matches_type(AsyncOffsetPagination[Credential], credential, 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: AsyncKernel) -> None: + credential = await async_client.credentials.delete( + "id", + ) + assert credential is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: + response = await async_client.credentials.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + credential = await response.parse() + assert credential is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None: + async with async_client.credentials.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + credential = await response.parse() + assert credential 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: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.credentials.with_raw_response.delete( + "", + ) From d2f8813c9116d487f362220a360bdd4ab87c43bf Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 04:10:01 +0000 Subject: [PATCH 241/448] chore: speedup initial import --- src/kernel/_client.py | 506 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 412 insertions(+), 94 deletions(-) diff --git a/src/kernel/_client.py b/src/kernel/_client.py index 4dc11976..166ecdb2 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -3,7 +3,7 @@ from __future__ import annotations import os -from typing import Any, Dict, Mapping, cast +from typing import TYPE_CHECKING, Any, Dict, Mapping, cast from typing_extensions import Self, Literal, override import httpx @@ -20,8 +20,8 @@ not_given, ) from ._utils import is_given, get_async_library +from ._compat import cached_property from ._version import __version__ -from .resources import apps, proxies, profiles, extensions, credentials, deployments, invocations, browser_pools from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import KernelError, APIStatusError from ._base_client import ( @@ -29,8 +29,30 @@ SyncAPIClient, AsyncAPIClient, ) -from .resources.agents import agents -from .resources.browsers import browsers + +if TYPE_CHECKING: + from .resources import ( + apps, + agents, + proxies, + browsers, + profiles, + extensions, + credentials, + deployments, + invocations, + browser_pools, + ) + from .resources.apps import AppsResource, AsyncAppsResource + from .resources.proxies import ProxiesResource, AsyncProxiesResource + from .resources.profiles import ProfilesResource, AsyncProfilesResource + from .resources.extensions import ExtensionsResource, AsyncExtensionsResource + from .resources.credentials import CredentialsResource, AsyncCredentialsResource + from .resources.deployments import DeploymentsResource, AsyncDeploymentsResource + from .resources.invocations import InvocationsResource, AsyncInvocationsResource + from .resources.agents.agents import AgentsResource, AsyncAgentsResource + from .resources.browser_pools import BrowserPoolsResource, AsyncBrowserPoolsResource + from .resources.browsers.browsers import BrowsersResource, AsyncBrowsersResource __all__ = [ "ENVIRONMENTS", @@ -51,19 +73,6 @@ class Kernel(SyncAPIClient): - deployments: deployments.DeploymentsResource - apps: apps.AppsResource - invocations: invocations.InvocationsResource - browsers: browsers.BrowsersResource - profiles: profiles.ProfilesResource - proxies: proxies.ProxiesResource - extensions: extensions.ExtensionsResource - browser_pools: browser_pools.BrowserPoolsResource - agents: agents.AgentsResource - credentials: credentials.CredentialsResource - with_raw_response: KernelWithRawResponse - with_streaming_response: KernelWithStreamedResponse - # client options api_key: str @@ -142,18 +151,73 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.deployments = deployments.DeploymentsResource(self) - self.apps = apps.AppsResource(self) - self.invocations = invocations.InvocationsResource(self) - self.browsers = browsers.BrowsersResource(self) - self.profiles = profiles.ProfilesResource(self) - self.proxies = proxies.ProxiesResource(self) - self.extensions = extensions.ExtensionsResource(self) - self.browser_pools = browser_pools.BrowserPoolsResource(self) - self.agents = agents.AgentsResource(self) - self.credentials = credentials.CredentialsResource(self) - self.with_raw_response = KernelWithRawResponse(self) - self.with_streaming_response = KernelWithStreamedResponse(self) + @cached_property + def deployments(self) -> DeploymentsResource: + from .resources.deployments import DeploymentsResource + + return DeploymentsResource(self) + + @cached_property + def apps(self) -> AppsResource: + from .resources.apps import AppsResource + + return AppsResource(self) + + @cached_property + def invocations(self) -> InvocationsResource: + from .resources.invocations import InvocationsResource + + return InvocationsResource(self) + + @cached_property + def browsers(self) -> BrowsersResource: + from .resources.browsers import BrowsersResource + + return BrowsersResource(self) + + @cached_property + def profiles(self) -> ProfilesResource: + from .resources.profiles import ProfilesResource + + return ProfilesResource(self) + + @cached_property + def proxies(self) -> ProxiesResource: + from .resources.proxies import ProxiesResource + + return ProxiesResource(self) + + @cached_property + def extensions(self) -> ExtensionsResource: + from .resources.extensions import ExtensionsResource + + return ExtensionsResource(self) + + @cached_property + def browser_pools(self) -> BrowserPoolsResource: + from .resources.browser_pools import BrowserPoolsResource + + return BrowserPoolsResource(self) + + @cached_property + def agents(self) -> AgentsResource: + from .resources.agents import AgentsResource + + return AgentsResource(self) + + @cached_property + def credentials(self) -> CredentialsResource: + from .resources.credentials import CredentialsResource + + return CredentialsResource(self) + + @cached_property + def with_raw_response(self) -> KernelWithRawResponse: + return KernelWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> KernelWithStreamedResponse: + return KernelWithStreamedResponse(self) @property @override @@ -263,19 +327,6 @@ def _make_status_error( class AsyncKernel(AsyncAPIClient): - deployments: deployments.AsyncDeploymentsResource - apps: apps.AsyncAppsResource - invocations: invocations.AsyncInvocationsResource - browsers: browsers.AsyncBrowsersResource - profiles: profiles.AsyncProfilesResource - proxies: proxies.AsyncProxiesResource - extensions: extensions.AsyncExtensionsResource - browser_pools: browser_pools.AsyncBrowserPoolsResource - agents: agents.AsyncAgentsResource - credentials: credentials.AsyncCredentialsResource - with_raw_response: AsyncKernelWithRawResponse - with_streaming_response: AsyncKernelWithStreamedResponse - # client options api_key: str @@ -354,18 +405,73 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.deployments = deployments.AsyncDeploymentsResource(self) - self.apps = apps.AsyncAppsResource(self) - self.invocations = invocations.AsyncInvocationsResource(self) - self.browsers = browsers.AsyncBrowsersResource(self) - self.profiles = profiles.AsyncProfilesResource(self) - self.proxies = proxies.AsyncProxiesResource(self) - self.extensions = extensions.AsyncExtensionsResource(self) - self.browser_pools = browser_pools.AsyncBrowserPoolsResource(self) - self.agents = agents.AsyncAgentsResource(self) - self.credentials = credentials.AsyncCredentialsResource(self) - self.with_raw_response = AsyncKernelWithRawResponse(self) - self.with_streaming_response = AsyncKernelWithStreamedResponse(self) + @cached_property + def deployments(self) -> AsyncDeploymentsResource: + from .resources.deployments import AsyncDeploymentsResource + + return AsyncDeploymentsResource(self) + + @cached_property + def apps(self) -> AsyncAppsResource: + from .resources.apps import AsyncAppsResource + + return AsyncAppsResource(self) + + @cached_property + def invocations(self) -> AsyncInvocationsResource: + from .resources.invocations import AsyncInvocationsResource + + return AsyncInvocationsResource(self) + + @cached_property + def browsers(self) -> AsyncBrowsersResource: + from .resources.browsers import AsyncBrowsersResource + + return AsyncBrowsersResource(self) + + @cached_property + def profiles(self) -> AsyncProfilesResource: + from .resources.profiles import AsyncProfilesResource + + return AsyncProfilesResource(self) + + @cached_property + def proxies(self) -> AsyncProxiesResource: + from .resources.proxies import AsyncProxiesResource + + return AsyncProxiesResource(self) + + @cached_property + def extensions(self) -> AsyncExtensionsResource: + from .resources.extensions import AsyncExtensionsResource + + return AsyncExtensionsResource(self) + + @cached_property + def browser_pools(self) -> AsyncBrowserPoolsResource: + from .resources.browser_pools import AsyncBrowserPoolsResource + + return AsyncBrowserPoolsResource(self) + + @cached_property + def agents(self) -> AsyncAgentsResource: + from .resources.agents import AsyncAgentsResource + + return AsyncAgentsResource(self) + + @cached_property + def credentials(self) -> AsyncCredentialsResource: + from .resources.credentials import AsyncCredentialsResource + + return AsyncCredentialsResource(self) + + @cached_property + def with_raw_response(self) -> AsyncKernelWithRawResponse: + return AsyncKernelWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncKernelWithStreamedResponse: + return AsyncKernelWithStreamedResponse(self) @property @override @@ -475,59 +581,271 @@ def _make_status_error( class KernelWithRawResponse: + _client: Kernel + def __init__(self, client: Kernel) -> None: - self.deployments = deployments.DeploymentsResourceWithRawResponse(client.deployments) - self.apps = apps.AppsResourceWithRawResponse(client.apps) - self.invocations = invocations.InvocationsResourceWithRawResponse(client.invocations) - self.browsers = browsers.BrowsersResourceWithRawResponse(client.browsers) - self.profiles = profiles.ProfilesResourceWithRawResponse(client.profiles) - self.proxies = proxies.ProxiesResourceWithRawResponse(client.proxies) - self.extensions = extensions.ExtensionsResourceWithRawResponse(client.extensions) - self.browser_pools = browser_pools.BrowserPoolsResourceWithRawResponse(client.browser_pools) - self.agents = agents.AgentsResourceWithRawResponse(client.agents) - self.credentials = credentials.CredentialsResourceWithRawResponse(client.credentials) + self._client = client + + @cached_property + def deployments(self) -> deployments.DeploymentsResourceWithRawResponse: + from .resources.deployments import DeploymentsResourceWithRawResponse + + return DeploymentsResourceWithRawResponse(self._client.deployments) + + @cached_property + def apps(self) -> apps.AppsResourceWithRawResponse: + from .resources.apps import AppsResourceWithRawResponse + + return AppsResourceWithRawResponse(self._client.apps) + + @cached_property + def invocations(self) -> invocations.InvocationsResourceWithRawResponse: + from .resources.invocations import InvocationsResourceWithRawResponse + + return InvocationsResourceWithRawResponse(self._client.invocations) + + @cached_property + def browsers(self) -> browsers.BrowsersResourceWithRawResponse: + from .resources.browsers import BrowsersResourceWithRawResponse + + return BrowsersResourceWithRawResponse(self._client.browsers) + + @cached_property + def profiles(self) -> profiles.ProfilesResourceWithRawResponse: + from .resources.profiles import ProfilesResourceWithRawResponse + + return ProfilesResourceWithRawResponse(self._client.profiles) + + @cached_property + def proxies(self) -> proxies.ProxiesResourceWithRawResponse: + from .resources.proxies import ProxiesResourceWithRawResponse + + return ProxiesResourceWithRawResponse(self._client.proxies) + + @cached_property + def extensions(self) -> extensions.ExtensionsResourceWithRawResponse: + from .resources.extensions import ExtensionsResourceWithRawResponse + + return ExtensionsResourceWithRawResponse(self._client.extensions) + + @cached_property + def browser_pools(self) -> browser_pools.BrowserPoolsResourceWithRawResponse: + from .resources.browser_pools import BrowserPoolsResourceWithRawResponse + + return BrowserPoolsResourceWithRawResponse(self._client.browser_pools) + + @cached_property + def agents(self) -> agents.AgentsResourceWithRawResponse: + from .resources.agents import AgentsResourceWithRawResponse + + return AgentsResourceWithRawResponse(self._client.agents) + + @cached_property + def credentials(self) -> credentials.CredentialsResourceWithRawResponse: + from .resources.credentials import CredentialsResourceWithRawResponse + + return CredentialsResourceWithRawResponse(self._client.credentials) class AsyncKernelWithRawResponse: + _client: AsyncKernel + def __init__(self, client: AsyncKernel) -> None: - self.deployments = deployments.AsyncDeploymentsResourceWithRawResponse(client.deployments) - self.apps = apps.AsyncAppsResourceWithRawResponse(client.apps) - self.invocations = invocations.AsyncInvocationsResourceWithRawResponse(client.invocations) - self.browsers = browsers.AsyncBrowsersResourceWithRawResponse(client.browsers) - self.profiles = profiles.AsyncProfilesResourceWithRawResponse(client.profiles) - self.proxies = proxies.AsyncProxiesResourceWithRawResponse(client.proxies) - self.extensions = extensions.AsyncExtensionsResourceWithRawResponse(client.extensions) - self.browser_pools = browser_pools.AsyncBrowserPoolsResourceWithRawResponse(client.browser_pools) - self.agents = agents.AsyncAgentsResourceWithRawResponse(client.agents) - self.credentials = credentials.AsyncCredentialsResourceWithRawResponse(client.credentials) + self._client = client + + @cached_property + def deployments(self) -> deployments.AsyncDeploymentsResourceWithRawResponse: + from .resources.deployments import AsyncDeploymentsResourceWithRawResponse + + return AsyncDeploymentsResourceWithRawResponse(self._client.deployments) + + @cached_property + def apps(self) -> apps.AsyncAppsResourceWithRawResponse: + from .resources.apps import AsyncAppsResourceWithRawResponse + + return AsyncAppsResourceWithRawResponse(self._client.apps) + + @cached_property + def invocations(self) -> invocations.AsyncInvocationsResourceWithRawResponse: + from .resources.invocations import AsyncInvocationsResourceWithRawResponse + + return AsyncInvocationsResourceWithRawResponse(self._client.invocations) + + @cached_property + def browsers(self) -> browsers.AsyncBrowsersResourceWithRawResponse: + from .resources.browsers import AsyncBrowsersResourceWithRawResponse + + return AsyncBrowsersResourceWithRawResponse(self._client.browsers) + + @cached_property + def profiles(self) -> profiles.AsyncProfilesResourceWithRawResponse: + from .resources.profiles import AsyncProfilesResourceWithRawResponse + + return AsyncProfilesResourceWithRawResponse(self._client.profiles) + + @cached_property + def proxies(self) -> proxies.AsyncProxiesResourceWithRawResponse: + from .resources.proxies import AsyncProxiesResourceWithRawResponse + + return AsyncProxiesResourceWithRawResponse(self._client.proxies) + + @cached_property + def extensions(self) -> extensions.AsyncExtensionsResourceWithRawResponse: + from .resources.extensions import AsyncExtensionsResourceWithRawResponse + + return AsyncExtensionsResourceWithRawResponse(self._client.extensions) + + @cached_property + def browser_pools(self) -> browser_pools.AsyncBrowserPoolsResourceWithRawResponse: + from .resources.browser_pools import AsyncBrowserPoolsResourceWithRawResponse + + return AsyncBrowserPoolsResourceWithRawResponse(self._client.browser_pools) + + @cached_property + def agents(self) -> agents.AsyncAgentsResourceWithRawResponse: + from .resources.agents import AsyncAgentsResourceWithRawResponse + + return AsyncAgentsResourceWithRawResponse(self._client.agents) + + @cached_property + def credentials(self) -> credentials.AsyncCredentialsResourceWithRawResponse: + from .resources.credentials import AsyncCredentialsResourceWithRawResponse + + return AsyncCredentialsResourceWithRawResponse(self._client.credentials) class KernelWithStreamedResponse: + _client: Kernel + def __init__(self, client: Kernel) -> None: - self.deployments = deployments.DeploymentsResourceWithStreamingResponse(client.deployments) - self.apps = apps.AppsResourceWithStreamingResponse(client.apps) - self.invocations = invocations.InvocationsResourceWithStreamingResponse(client.invocations) - self.browsers = browsers.BrowsersResourceWithStreamingResponse(client.browsers) - self.profiles = profiles.ProfilesResourceWithStreamingResponse(client.profiles) - self.proxies = proxies.ProxiesResourceWithStreamingResponse(client.proxies) - self.extensions = extensions.ExtensionsResourceWithStreamingResponse(client.extensions) - self.browser_pools = browser_pools.BrowserPoolsResourceWithStreamingResponse(client.browser_pools) - self.agents = agents.AgentsResourceWithStreamingResponse(client.agents) - self.credentials = credentials.CredentialsResourceWithStreamingResponse(client.credentials) + self._client = client + + @cached_property + def deployments(self) -> deployments.DeploymentsResourceWithStreamingResponse: + from .resources.deployments import DeploymentsResourceWithStreamingResponse + + return DeploymentsResourceWithStreamingResponse(self._client.deployments) + + @cached_property + def apps(self) -> apps.AppsResourceWithStreamingResponse: + from .resources.apps import AppsResourceWithStreamingResponse + + return AppsResourceWithStreamingResponse(self._client.apps) + + @cached_property + def invocations(self) -> invocations.InvocationsResourceWithStreamingResponse: + from .resources.invocations import InvocationsResourceWithStreamingResponse + + return InvocationsResourceWithStreamingResponse(self._client.invocations) + + @cached_property + def browsers(self) -> browsers.BrowsersResourceWithStreamingResponse: + from .resources.browsers import BrowsersResourceWithStreamingResponse + + return BrowsersResourceWithStreamingResponse(self._client.browsers) + + @cached_property + def profiles(self) -> profiles.ProfilesResourceWithStreamingResponse: + from .resources.profiles import ProfilesResourceWithStreamingResponse + + return ProfilesResourceWithStreamingResponse(self._client.profiles) + + @cached_property + def proxies(self) -> proxies.ProxiesResourceWithStreamingResponse: + from .resources.proxies import ProxiesResourceWithStreamingResponse + + return ProxiesResourceWithStreamingResponse(self._client.proxies) + + @cached_property + def extensions(self) -> extensions.ExtensionsResourceWithStreamingResponse: + from .resources.extensions import ExtensionsResourceWithStreamingResponse + + return ExtensionsResourceWithStreamingResponse(self._client.extensions) + + @cached_property + def browser_pools(self) -> browser_pools.BrowserPoolsResourceWithStreamingResponse: + from .resources.browser_pools import BrowserPoolsResourceWithStreamingResponse + + return BrowserPoolsResourceWithStreamingResponse(self._client.browser_pools) + + @cached_property + def agents(self) -> agents.AgentsResourceWithStreamingResponse: + from .resources.agents import AgentsResourceWithStreamingResponse + + return AgentsResourceWithStreamingResponse(self._client.agents) + + @cached_property + def credentials(self) -> credentials.CredentialsResourceWithStreamingResponse: + from .resources.credentials import CredentialsResourceWithStreamingResponse + + return CredentialsResourceWithStreamingResponse(self._client.credentials) class AsyncKernelWithStreamedResponse: + _client: AsyncKernel + def __init__(self, client: AsyncKernel) -> None: - self.deployments = deployments.AsyncDeploymentsResourceWithStreamingResponse(client.deployments) - self.apps = apps.AsyncAppsResourceWithStreamingResponse(client.apps) - self.invocations = invocations.AsyncInvocationsResourceWithStreamingResponse(client.invocations) - self.browsers = browsers.AsyncBrowsersResourceWithStreamingResponse(client.browsers) - self.profiles = profiles.AsyncProfilesResourceWithStreamingResponse(client.profiles) - self.proxies = proxies.AsyncProxiesResourceWithStreamingResponse(client.proxies) - self.extensions = extensions.AsyncExtensionsResourceWithStreamingResponse(client.extensions) - self.browser_pools = browser_pools.AsyncBrowserPoolsResourceWithStreamingResponse(client.browser_pools) - self.agents = agents.AsyncAgentsResourceWithStreamingResponse(client.agents) - self.credentials = credentials.AsyncCredentialsResourceWithStreamingResponse(client.credentials) + self._client = client + + @cached_property + def deployments(self) -> deployments.AsyncDeploymentsResourceWithStreamingResponse: + from .resources.deployments import AsyncDeploymentsResourceWithStreamingResponse + + return AsyncDeploymentsResourceWithStreamingResponse(self._client.deployments) + + @cached_property + def apps(self) -> apps.AsyncAppsResourceWithStreamingResponse: + from .resources.apps import AsyncAppsResourceWithStreamingResponse + + return AsyncAppsResourceWithStreamingResponse(self._client.apps) + + @cached_property + def invocations(self) -> invocations.AsyncInvocationsResourceWithStreamingResponse: + from .resources.invocations import AsyncInvocationsResourceWithStreamingResponse + + return AsyncInvocationsResourceWithStreamingResponse(self._client.invocations) + + @cached_property + def browsers(self) -> browsers.AsyncBrowsersResourceWithStreamingResponse: + from .resources.browsers import AsyncBrowsersResourceWithStreamingResponse + + return AsyncBrowsersResourceWithStreamingResponse(self._client.browsers) + + @cached_property + def profiles(self) -> profiles.AsyncProfilesResourceWithStreamingResponse: + from .resources.profiles import AsyncProfilesResourceWithStreamingResponse + + return AsyncProfilesResourceWithStreamingResponse(self._client.profiles) + + @cached_property + def proxies(self) -> proxies.AsyncProxiesResourceWithStreamingResponse: + from .resources.proxies import AsyncProxiesResourceWithStreamingResponse + + return AsyncProxiesResourceWithStreamingResponse(self._client.proxies) + + @cached_property + def extensions(self) -> extensions.AsyncExtensionsResourceWithStreamingResponse: + from .resources.extensions import AsyncExtensionsResourceWithStreamingResponse + + return AsyncExtensionsResourceWithStreamingResponse(self._client.extensions) + + @cached_property + def browser_pools(self) -> browser_pools.AsyncBrowserPoolsResourceWithStreamingResponse: + from .resources.browser_pools import AsyncBrowserPoolsResourceWithStreamingResponse + + return AsyncBrowserPoolsResourceWithStreamingResponse(self._client.browser_pools) + + @cached_property + def agents(self) -> agents.AsyncAgentsResourceWithStreamingResponse: + from .resources.agents import AsyncAgentsResourceWithStreamingResponse + + return AsyncAgentsResourceWithStreamingResponse(self._client.agents) + + @cached_property + def credentials(self) -> credentials.AsyncCredentialsResourceWithStreamingResponse: + from .resources.credentials import AsyncCredentialsResourceWithStreamingResponse + + return AsyncCredentialsResourceWithStreamingResponse(self._client.credentials) Client = Kernel From 324e09e1e0a6b12f602502ca04356a1b7e3da552 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 04:35:14 +0000 Subject: [PATCH 242/448] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 0a02606e..299cb5a6 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 89 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-486d57f189abcec3a678ad4a619ee8a6b8aec3a3c2f3620c0423cb16cc755a13.yml -openapi_spec_hash: affde047293fc74a8343a121d5e58a9c +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-13214b99e392aab631aa1ca99b6a51a58df81e34156d21b8d639bea779566123.yml +openapi_spec_hash: a88d175fc3980de3097ac1411d8dcbff config_hash: 7225e7b7e4695c81d7be26c7108b5494 From e448da84baf2b3987a9ff6f065130e4851070988 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 23:31:22 +0000 Subject: [PATCH 243/448] feat: Fix browser pool sdk types --- .stats.yml | 2 +- api.md | 10 +-- src/kernel/types/__init__.py | 1 - src/kernel/types/browser_pool.py | 75 +++++++++++++++++++++-- src/kernel/types/browser_pool_request.py | 78 ------------------------ 5 files changed, 73 insertions(+), 93 deletions(-) delete mode 100644 src/kernel/types/browser_pool_request.py diff --git a/.stats.yml b/.stats.yml index 299cb5a6..99bb6c41 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 89 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-13214b99e392aab631aa1ca99b6a51a58df81e34156d21b8d639bea779566123.yml openapi_spec_hash: a88d175fc3980de3097ac1411d8dcbff -config_hash: 7225e7b7e4695c81d7be26c7108b5494 +config_hash: 179f33af31ece83563163d5b3d751d13 diff --git a/api.md b/api.md index d5c3bc62..fb67de54 100644 --- a/api.md +++ b/api.md @@ -259,15 +259,7 @@ Methods: Types: ```python -from kernel.types import ( - BrowserPool, - BrowserPoolAcquireRequest, - BrowserPoolReleaseRequest, - BrowserPoolRequest, - BrowserPoolUpdateRequest, - BrowserPoolListResponse, - BrowserPoolAcquireResponse, -) +from kernel.types import BrowserPool, BrowserPoolListResponse, BrowserPoolAcquireResponse ``` Methods: diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index b5a9804e..5fad2167 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -22,7 +22,6 @@ from .browser_persistence import BrowserPersistence as BrowserPersistence from .proxy_create_params import ProxyCreateParams as ProxyCreateParams from .proxy_list_response import ProxyListResponse as ProxyListResponse -from .browser_pool_request import BrowserPoolRequest as BrowserPoolRequest from .browser_create_params import BrowserCreateParams as BrowserCreateParams from .browser_delete_params import BrowserDeleteParams as BrowserDeleteParams from .browser_list_response import BrowserListResponse as BrowserListResponse diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py index ddd3d9f4..1694313c 100644 --- a/src/kernel/types/browser_pool.py +++ b/src/kernel/types/browser_pool.py @@ -1,12 +1,79 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional +from typing import List, Optional from datetime import datetime from .._models import BaseModel -from .browser_pool_request import BrowserPoolRequest +from .shared.browser_profile import BrowserProfile +from .shared.browser_viewport import BrowserViewport +from .shared.browser_extension import BrowserExtension -__all__ = ["BrowserPool"] +__all__ = ["BrowserPool", "BrowserPoolConfig"] + + +class BrowserPoolConfig(BaseModel): + """Configuration used to create all browsers in this pool""" + + size: int + """Number of browsers to create in the pool""" + + extensions: Optional[List[BrowserExtension]] = None + """List of browser extensions to load into the session. + + Provide each by id or name. + """ + + fill_rate_per_minute: Optional[int] = None + """Percentage of the pool to fill per minute. Defaults to 10%.""" + + headless: Optional[bool] = None + """If true, launches the browser using a headless image. Defaults to false.""" + + kiosk_mode: Optional[bool] = None + """ + If true, launches the browser in kiosk mode to hide address bar and tabs in live + view. + """ + + name: Optional[str] = None + """Optional name for the browser pool. Must be unique within the organization.""" + + profile: Optional[BrowserProfile] = None + """Profile selection for the browser session. + + Provide either id or name. If specified, the matching profile will be loaded + into the browser session. Profiles must be created beforehand. + """ + + proxy_id: Optional[str] = None + """Optional proxy to associate to the browser session. + + Must reference a proxy belonging to the caller's org. + """ + + stealth: Optional[bool] = None + """ + If true, launches the browser in stealth mode to reduce detection by anti-bot + mechanisms. + """ + + timeout_seconds: Optional[int] = None + """ + Default idle timeout in seconds for browsers acquired from this pool before they + are destroyed. Defaults to 600 seconds if not specified + """ + + viewport: Optional[BrowserViewport] = None + """Initial browser window size in pixels with optional refresh rate. + + If omitted, image defaults apply (1920x1080@25). Only specific viewport + configurations are supported. The server will reject unsupported combinations. + Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will + be automatically determined from the width and height if they match a supported + configuration exactly. Note: Higher resolutions may affect the responsiveness of + live view browser + """ class BrowserPool(BaseModel): @@ -21,7 +88,7 @@ class BrowserPool(BaseModel): available_count: int """Number of browsers currently available in the pool""" - browser_pool_config: BrowserPoolRequest + browser_pool_config: BrowserPoolConfig """Configuration used to create all browsers in this pool""" created_at: datetime diff --git a/src/kernel/types/browser_pool_request.py b/src/kernel/types/browser_pool_request.py deleted file mode 100644 index a392b3fb..00000000 --- a/src/kernel/types/browser_pool_request.py +++ /dev/null @@ -1,78 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Optional - -from .._models import BaseModel -from .shared.browser_profile import BrowserProfile -from .shared.browser_viewport import BrowserViewport -from .shared.browser_extension import BrowserExtension - -__all__ = ["BrowserPoolRequest"] - - -class BrowserPoolRequest(BaseModel): - """Parameters for creating a browser pool. - - All browsers in the pool will be created with the same configuration. - """ - - size: int - """Number of browsers to create in the pool""" - - extensions: Optional[List[BrowserExtension]] = None - """List of browser extensions to load into the session. - - Provide each by id or name. - """ - - fill_rate_per_minute: Optional[int] = None - """Percentage of the pool to fill per minute. Defaults to 10%.""" - - headless: Optional[bool] = None - """If true, launches the browser using a headless image. Defaults to false.""" - - kiosk_mode: Optional[bool] = None - """ - If true, launches the browser in kiosk mode to hide address bar and tabs in live - view. - """ - - name: Optional[str] = None - """Optional name for the browser pool. Must be unique within the organization.""" - - profile: Optional[BrowserProfile] = None - """Profile selection for the browser session. - - Provide either id or name. If specified, the matching profile will be loaded - into the browser session. Profiles must be created beforehand. - """ - - proxy_id: Optional[str] = None - """Optional proxy to associate to the browser session. - - Must reference a proxy belonging to the caller's org. - """ - - stealth: Optional[bool] = None - """ - If true, launches the browser in stealth mode to reduce detection by anti-bot - mechanisms. - """ - - timeout_seconds: Optional[int] = None - """ - Default idle timeout in seconds for browsers acquired from this pool before they - are destroyed. Defaults to 600 seconds if not specified - """ - - viewport: Optional[BrowserViewport] = None - """Initial browser window size in pixels with optional refresh rate. - - If omitted, image defaults apply (1920x1080@25). Only specific viewport - configurations are supported. The server will reject unsupported combinations. - Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will - be automatically determined from the width and height if they match a supported - configuration exactly. Note: Higher resolutions may affect the responsiveness of - live view browser - """ From e6410e96603e54cc7f8c09e3752f6768cbf47b8f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 23:34:20 +0000 Subject: [PATCH 244/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 7f3f5c84..d2d60a3d 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.23.0" + ".": "0.24.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 6e76a589..770392c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.23.0" +version = "0.24.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 6e518416..17d46b5d 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.23.0" # x-release-please-version +__version__ = "0.24.0" # x-release-please-version From c168aa63bdb24c8ee0c017939d53c132308a4330 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 18 Dec 2025 04:31:03 +0000 Subject: [PATCH 245/448] fix: use async_to_httpx_files in patch method --- src/kernel/_base_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kernel/_base_client.py b/src/kernel/_base_client.py index e55218be..787be54c 100644 --- a/src/kernel/_base_client.py +++ b/src/kernel/_base_client.py @@ -1774,7 +1774,7 @@ async def patch( options: RequestOptions = {}, ) -> ResponseT: opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts) From 76946d06ad916f9bf7acf9005628da3910adbd7d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 04:09:53 +0000 Subject: [PATCH 246/448] chore(internal): add `--fix` argument to lint script --- scripts/lint | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/lint b/scripts/lint index b5b88913..7675e607 100755 --- a/scripts/lint +++ b/scripts/lint @@ -4,8 +4,13 @@ set -e cd "$(dirname "$0")/.." -echo "==> Running lints" -rye run lint +if [ "$1" = "--fix" ]; then + echo "==> Running lints with --fix" + rye run fix:ruff +else + echo "==> Running lints" + rye run lint +fi echo "==> Making sure it imports" rye run python -c 'import kernel' From aad735f6c663aabbee71b8d4bf0bf2a233e4a462 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 20 Dec 2025 00:26:34 +0000 Subject: [PATCH 247/448] feat: Enhance AuthAgentInvocation with step and last activity tracking --- .stats.yml | 4 ++-- .../resources/agents/auth/invocations.py | 24 +++++++++---------- .../agents/agent_auth_invocation_response.py | 3 +++ .../auth_agent_invocation_create_response.py | 4 ++-- src/kernel/types/agents/reauth_response.py | 2 +- 5 files changed, 20 insertions(+), 17 deletions(-) diff --git a/.stats.yml b/.stats.yml index 99bb6c41..91fb4072 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 89 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-13214b99e392aab631aa1ca99b6a51a58df81e34156d21b8d639bea779566123.yml -openapi_spec_hash: a88d175fc3980de3097ac1411d8dcbff +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2cf104c4b88159c6a50713b019188f83983748b9cacec598089cf9068dc5b1cd.yml +openapi_spec_hash: 84ea30ae65ad7ebcc04d2f3907d1e73b config_hash: 179f33af31ece83563163d5b3d751d13 diff --git a/src/kernel/resources/agents/auth/invocations.py b/src/kernel/resources/agents/auth/invocations.py index ac2bb8ed..f5e60533 100644 --- a/src/kernel/resources/agents/auth/invocations.py +++ b/src/kernel/resources/agents/auth/invocations.py @@ -116,10 +116,9 @@ def retrieve( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AgentAuthInvocationResponse: - """Returns invocation details including app_name and target_domain. - - Uses the JWT - returned by the exchange endpoint, or standard API key or JWT authentication. + """ + Returns invocation details including status, app_name, and target_domain. + Supports both API key and JWT (from exchange endpoint) authentication. Args: extra_headers: Send extra headers @@ -155,7 +154,7 @@ def discover( """ Inspects the target site to detect logged-in state or discover required fields. Returns 200 with success: true when fields are found, or 4xx/5xx for failures. - Requires the JWT returned by the exchange endpoint. + Supports both API key and JWT (from exchange endpoint) authentication. Args: login_url: Optional login page URL. If provided, will override the stored login URL for @@ -233,7 +232,8 @@ def submit( ) -> AgentAuthSubmitResponse: """ Submits field values for the discovered login form and may return additional - auth fields or success. Requires the JWT returned by the exchange endpoint. + auth fields or success. Supports both API key and JWT (from exchange endpoint) + authentication. Args: field_values: Values for the discovered login fields @@ -342,10 +342,9 @@ async def retrieve( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AgentAuthInvocationResponse: - """Returns invocation details including app_name and target_domain. - - Uses the JWT - returned by the exchange endpoint, or standard API key or JWT authentication. + """ + Returns invocation details including status, app_name, and target_domain. + Supports both API key and JWT (from exchange endpoint) authentication. Args: extra_headers: Send extra headers @@ -381,7 +380,7 @@ async def discover( """ Inspects the target site to detect logged-in state or discover required fields. Returns 200 with success: true when fields are found, or 4xx/5xx for failures. - Requires the JWT returned by the exchange endpoint. + Supports both API key and JWT (from exchange endpoint) authentication. Args: login_url: Optional login page URL. If provided, will override the stored login URL for @@ -461,7 +460,8 @@ async def submit( ) -> AgentAuthSubmitResponse: """ Submits field values for the discovered login form and may return additional - auth fields or success. Requires the JWT returned by the exchange endpoint. + auth fields or success. Supports both API key and JWT (from exchange endpoint) + authentication. Args: field_values: Values for the discovered login fields diff --git a/src/kernel/types/agents/agent_auth_invocation_response.py b/src/kernel/types/agents/agent_auth_invocation_response.py index 02b5ecf9..3ddfe8e2 100644 --- a/src/kernel/types/agents/agent_auth_invocation_response.py +++ b/src/kernel/types/agents/agent_auth_invocation_response.py @@ -20,5 +20,8 @@ class AgentAuthInvocationResponse(BaseModel): status: Literal["IN_PROGRESS", "SUCCESS", "EXPIRED", "CANCELED"] """Invocation status""" + step: Literal["initialized", "discovering", "awaiting_input", "submitting", "completed", "expired"] + """Current step in the invocation workflow""" + target_domain: str """Target domain for authentication""" diff --git a/src/kernel/types/agents/auth_agent_invocation_create_response.py b/src/kernel/types/agents/auth_agent_invocation_create_response.py index da0b6f64..b2a5e20c 100644 --- a/src/kernel/types/agents/auth_agent_invocation_create_response.py +++ b/src/kernel/types/agents/auth_agent_invocation_create_response.py @@ -13,7 +13,7 @@ class AuthAgentAlreadyAuthenticated(BaseModel): """Response when the agent is already authenticated.""" - status: Literal["already_authenticated"] + status: Literal["ALREADY_AUTHENTICATED"] """Indicates the agent is already authenticated and no invocation was created.""" @@ -32,7 +32,7 @@ class AuthAgentInvocationCreated(BaseModel): invocation_id: str """Unique identifier for the invocation.""" - status: Literal["invocation_created"] + status: Literal["INVOCATION_CREATED"] """Indicates an invocation was created.""" diff --git a/src/kernel/types/agents/reauth_response.py b/src/kernel/types/agents/reauth_response.py index 4ead46a4..4fbf0e47 100644 --- a/src/kernel/types/agents/reauth_response.py +++ b/src/kernel/types/agents/reauth_response.py @@ -11,7 +11,7 @@ class ReauthResponse(BaseModel): """Response from triggering re-authentication""" - status: Literal["reauth_started", "already_authenticated", "cannot_reauth"] + status: Literal["REAUTH_STARTED", "ALREADY_AUTHENTICATED", "CANNOT_REAUTH"] """Result of the re-authentication attempt""" invocation_id: Optional[str] = None From 93192ecbb7249004a6c41e696b9238232220297b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 30 Dec 2025 21:51:16 +0000 Subject: [PATCH 248/448] feat(api): add health check endpoint for proxies --- .stats.yml | 8 +- api.md | 8 +- src/kernel/resources/proxies.py | 79 +++++++++ src/kernel/types/__init__.py | 1 + src/kernel/types/proxy_check_response.py | 195 +++++++++++++++++++++++ tests/api_resources/test_proxies.py | 91 ++++++++++- 6 files changed, 376 insertions(+), 6 deletions(-) create mode 100644 src/kernel/types/proxy_check_response.py diff --git a/.stats.yml b/.stats.yml index 91fb4072..579e071d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 89 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2cf104c4b88159c6a50713b019188f83983748b9cacec598089cf9068dc5b1cd.yml -openapi_spec_hash: 84ea30ae65ad7ebcc04d2f3907d1e73b -config_hash: 179f33af31ece83563163d5b3d751d13 +configured_endpoints: 90 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-20fac779e9e13dc9421e467be31dbf274c39072ba0c01528ba451b48698d43c1.yml +openapi_spec_hash: c3fc5784297ccc8f729326b62000d1f0 +config_hash: e47e015528251ee83e30367dbbb51044 diff --git a/api.md b/api.md index fb67de54..848c1042 100644 --- a/api.md +++ b/api.md @@ -228,7 +228,12 @@ Methods: Types: ```python -from kernel.types import ProxyCreateResponse, ProxyRetrieveResponse, ProxyListResponse +from kernel.types import ( + ProxyCreateResponse, + ProxyRetrieveResponse, + ProxyListResponse, + ProxyCheckResponse, +) ``` Methods: @@ -237,6 +242,7 @@ Methods: - client.proxies.retrieve(id) -> ProxyRetrieveResponse - client.proxies.list() -> ProxyListResponse - client.proxies.delete(id) -> None +- client.proxies.check(id) -> ProxyCheckResponse # Extensions diff --git a/src/kernel/resources/proxies.py b/src/kernel/resources/proxies.py index ba6862f8..4908ab79 100644 --- a/src/kernel/resources/proxies.py +++ b/src/kernel/resources/proxies.py @@ -19,6 +19,7 @@ ) from .._base_client import make_request_options from ..types.proxy_list_response import ProxyListResponse +from ..types.proxy_check_response import ProxyCheckResponse from ..types.proxy_create_response import ProxyCreateResponse from ..types.proxy_retrieve_response import ProxyRetrieveResponse @@ -184,6 +185,39 @@ def delete( cast_to=NoneType, ) + def check( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ProxyCheckResponse: + """ + Run a health check on the proxy to verify it's working. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/proxies/{id}/check", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProxyCheckResponse, + ) + class AsyncProxiesResource(AsyncAPIResource): @cached_property @@ -344,6 +378,39 @@ async def delete( cast_to=NoneType, ) + async def check( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ProxyCheckResponse: + """ + Run a health check on the proxy to verify it's working. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/proxies/{id}/check", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProxyCheckResponse, + ) + class ProxiesResourceWithRawResponse: def __init__(self, proxies: ProxiesResource) -> None: @@ -361,6 +428,9 @@ def __init__(self, proxies: ProxiesResource) -> None: self.delete = to_raw_response_wrapper( proxies.delete, ) + self.check = to_raw_response_wrapper( + proxies.check, + ) class AsyncProxiesResourceWithRawResponse: @@ -379,6 +449,9 @@ def __init__(self, proxies: AsyncProxiesResource) -> None: self.delete = async_to_raw_response_wrapper( proxies.delete, ) + self.check = async_to_raw_response_wrapper( + proxies.check, + ) class ProxiesResourceWithStreamingResponse: @@ -397,6 +470,9 @@ def __init__(self, proxies: ProxiesResource) -> None: self.delete = to_streamed_response_wrapper( proxies.delete, ) + self.check = to_streamed_response_wrapper( + proxies.check, + ) class AsyncProxiesResourceWithStreamingResponse: @@ -415,3 +491,6 @@ def __init__(self, proxies: AsyncProxiesResource) -> None: self.delete = async_to_streamed_response_wrapper( proxies.delete, ) + self.check = async_to_streamed_response_wrapper( + proxies.check, + ) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 5fad2167..1748bf22 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -22,6 +22,7 @@ from .browser_persistence import BrowserPersistence as BrowserPersistence from .proxy_create_params import ProxyCreateParams as ProxyCreateParams from .proxy_list_response import ProxyListResponse as ProxyListResponse +from .proxy_check_response import ProxyCheckResponse as ProxyCheckResponse from .browser_create_params import BrowserCreateParams as BrowserCreateParams from .browser_delete_params import BrowserDeleteParams as BrowserDeleteParams from .browser_list_response import BrowserListResponse as BrowserListResponse diff --git a/src/kernel/types/proxy_check_response.py b/src/kernel/types/proxy_check_response.py new file mode 100644 index 00000000..dc45f4f5 --- /dev/null +++ b/src/kernel/types/proxy_check_response.py @@ -0,0 +1,195 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Union, Optional +from datetime import datetime +from typing_extensions import Literal, TypeAlias + +from .._models import BaseModel + +__all__ = [ + "ProxyCheckResponse", + "Config", + "ConfigDatacenterProxyConfig", + "ConfigIspProxyConfig", + "ConfigResidentialProxyConfig", + "ConfigMobileProxyConfig", + "ConfigCustomProxyConfig", +] + + +class ConfigDatacenterProxyConfig(BaseModel): + """Configuration for a datacenter proxy.""" + + country: Optional[str] = None + """ISO 3166 country code. Defaults to US if not provided.""" + + +class ConfigIspProxyConfig(BaseModel): + """Configuration for an ISP proxy.""" + + country: Optional[str] = None + """ISO 3166 country code. Defaults to US if not provided.""" + + +class ConfigResidentialProxyConfig(BaseModel): + """Configuration for residential proxies.""" + + asn: Optional[str] = None + """Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html""" + + city: Optional[str] = None + """City name (no spaces, e.g. + + `sanfrancisco`). If provided, `country` must also be provided. + """ + + country: Optional[str] = None + """ISO 3166 country code.""" + + os: Optional[Literal["windows", "macos", "android"]] = None + """Operating system of the residential device.""" + + state: Optional[str] = None + """Two-letter state code.""" + + zip: Optional[str] = None + """US ZIP code.""" + + +class ConfigMobileProxyConfig(BaseModel): + """Configuration for mobile proxies.""" + + asn: Optional[str] = None + """Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html""" + + carrier: Optional[ + Literal[ + "a1", + "aircel", + "airtel", + "att", + "celcom", + "chinamobile", + "claro", + "comcast", + "cox", + "digi", + "dt", + "docomo", + "dtac", + "etisalat", + "idea", + "kyivstar", + "meo", + "megafon", + "mtn", + "mtnza", + "mts", + "optus", + "orange", + "qwest", + "reliance_jio", + "robi", + "sprint", + "telefonica", + "telstra", + "tmobile", + "tigo", + "tim", + "verizon", + "vimpelcom", + "vodacomza", + "vodafone", + "vivo", + "zain", + "vivabo", + "telenormyanmar", + "kcelljsc", + "swisscom", + "singtel", + "asiacell", + "windit", + "cellc", + "ooredoo", + "drei", + "umobile", + "cableone", + "proximus", + "tele2", + "mobitel", + "o2", + "bouygues", + "free", + "sfr", + "digicel", + ] + ] = None + """Mobile carrier.""" + + city: Optional[str] = None + """City name (no spaces, e.g. + + `sanfrancisco`). If provided, `country` must also be provided. + """ + + country: Optional[str] = None + """ISO 3166 country code""" + + state: Optional[str] = None + """Two-letter state code.""" + + zip: Optional[str] = None + """US ZIP code.""" + + +class ConfigCustomProxyConfig(BaseModel): + """Configuration for a custom proxy (e.g., private proxy server).""" + + host: str + """Proxy host address or IP.""" + + port: int + """Proxy port.""" + + has_password: Optional[bool] = None + """Whether the proxy has a password.""" + + username: Optional[str] = None + """Username for proxy authentication.""" + + +Config: TypeAlias = Union[ + ConfigDatacenterProxyConfig, + ConfigIspProxyConfig, + ConfigResidentialProxyConfig, + ConfigMobileProxyConfig, + ConfigCustomProxyConfig, +] + + +class ProxyCheckResponse(BaseModel): + """Configuration for routing traffic through a proxy.""" + + type: Literal["datacenter", "isp", "residential", "mobile", "custom"] + """Proxy type to use. + + In terms of quality for avoiding bot-detection, from best to worst: `mobile` > + `residential` > `isp` > `datacenter`. + """ + + id: Optional[str] = None + + config: Optional[Config] = None + """Configuration specific to the selected proxy `type`.""" + + last_checked: Optional[datetime] = None + """Timestamp of the last health check performed on this proxy.""" + + name: Optional[str] = None + """Readable name of the proxy.""" + + protocol: Optional[Literal["http", "https"]] = None + """Protocol to use for the proxy connection.""" + + status: Optional[Literal["available", "unavailable"]] = None + """Current health status of the proxy.""" diff --git a/tests/api_resources/test_proxies.py b/tests/api_resources/test_proxies.py index 484848fe..ed858e8d 100644 --- a/tests/api_resources/test_proxies.py +++ b/tests/api_resources/test_proxies.py @@ -9,7 +9,12 @@ from kernel import Kernel, AsyncKernel from tests.utils import assert_matches_type -from kernel.types import ProxyListResponse, ProxyCreateResponse, ProxyRetrieveResponse +from kernel.types import ( + ProxyListResponse, + ProxyCheckResponse, + ProxyCreateResponse, + ProxyRetrieveResponse, +) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -174,6 +179,48 @@ def test_path_params_delete(self, client: Kernel) -> None: "", ) + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_check(self, client: Kernel) -> None: + proxy = client.proxies.check( + "id", + ) + assert_matches_type(ProxyCheckResponse, proxy, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_check(self, client: Kernel) -> None: + response = client.proxies.with_raw_response.check( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + proxy = response.parse() + assert_matches_type(ProxyCheckResponse, proxy, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_check(self, client: Kernel) -> None: + with client.proxies.with_streaming_response.check( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + proxy = response.parse() + assert_matches_type(ProxyCheckResponse, proxy, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_check(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.proxies.with_raw_response.check( + "", + ) + class TestAsyncProxies: parametrize = pytest.mark.parametrize( @@ -336,3 +383,45 @@ async def test_path_params_delete(self, async_client: AsyncKernel) -> None: await async_client.proxies.with_raw_response.delete( "", ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_check(self, async_client: AsyncKernel) -> None: + proxy = await async_client.proxies.check( + "id", + ) + assert_matches_type(ProxyCheckResponse, proxy, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_check(self, async_client: AsyncKernel) -> None: + response = await async_client.proxies.with_raw_response.check( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + proxy = await response.parse() + assert_matches_type(ProxyCheckResponse, proxy, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_check(self, async_client: AsyncKernel) -> None: + async with async_client.proxies.with_streaming_response.check( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + proxy = await response.parse() + assert_matches_type(ProxyCheckResponse, proxy, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_check(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.proxies.with_raw_response.check( + "", + ) From 8b4fe9a80b0f67fff4bb2985815a6bddcb7bf44d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 03:49:42 +0000 Subject: [PATCH 249/448] chore(internal): codegen related update --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index b32a077a..3b7d20d9 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2025 Kernel + Copyright 2026 Kernel Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From d86bdb34a38ebfc6d61f037edf6e132f0e625ee2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 7 Jan 2026 04:53:17 +0000 Subject: [PATCH 250/448] feat(auth): add auto_login credential flow --- .stats.yml | 8 +- api.md | 18 +- src/kernel/resources/agents/auth/auth.py | 131 ++------- .../resources/agents/auth/invocations.py | 273 +++++++++--------- src/kernel/resources/credentials.py | 211 +++++++++++--- src/kernel/types/__init__.py | 1 + src/kernel/types/agents/__init__.py | 2 - .../agents/agent_auth_discover_response.py | 30 -- .../agents/agent_auth_invocation_response.py | 58 +++- .../agents/agent_auth_submit_response.py | 28 +- src/kernel/types/agents/auth/__init__.py | 1 - .../agents/auth/invocation_discover_params.py | 16 - .../agents/auth/invocation_submit_params.py | 16 +- src/kernel/types/agents/auth_agent.py | 9 +- .../auth_agent_invocation_create_response.py | 29 +- src/kernel/types/agents/auth_create_params.py | 13 +- src/kernel/types/agents/auth_list_params.py | 6 +- src/kernel/types/agents/discovered_field.py | 2 +- src/kernel/types/agents/reauth_response.py | 21 -- src/kernel/types/credential.py | 21 ++ src/kernel/types/credential_create_params.py | 14 + .../types/credential_totp_code_response.py | 15 + src/kernel/types/credential_update_params.py | 21 +- .../agents/auth/test_invocations.py | 217 +++++++------- tests/api_resources/agents/test_auth.py | 108 +------ tests/api_resources/test_credentials.py | 179 ++++++++++-- 26 files changed, 783 insertions(+), 665 deletions(-) delete mode 100644 src/kernel/types/agents/agent_auth_discover_response.py delete mode 100644 src/kernel/types/agents/auth/invocation_discover_params.py delete mode 100644 src/kernel/types/agents/reauth_response.py create mode 100644 src/kernel/types/credential_totp_code_response.py diff --git a/.stats.yml b/.stats.yml index 579e071d..434275ee 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 90 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-20fac779e9e13dc9421e467be31dbf274c39072ba0c01528ba451b48698d43c1.yml -openapi_spec_hash: c3fc5784297ccc8f729326b62000d1f0 -config_hash: e47e015528251ee83e30367dbbb51044 +configured_endpoints: 89 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-8d66dbedea5b240936b338809f272568ca84a452fc13dbda835479f2ec068b41.yml +openapi_spec_hash: 7c499bfce2e996f1fff5e7791cea390e +config_hash: fcc2db3ed48ab4e8d1b588d31d394a23 diff --git a/api.md b/api.md index 848c1042..db5f2dfb 100644 --- a/api.md +++ b/api.md @@ -287,7 +287,6 @@ Types: ```python from kernel.types.agents import ( - AgentAuthDiscoverResponse, AgentAuthInvocationResponse, AgentAuthSubmitResponse, AuthAgent, @@ -295,7 +294,6 @@ from kernel.types.agents import ( AuthAgentInvocationCreateRequest, AuthAgentInvocationCreateResponse, DiscoveredField, - ReauthResponse, ) ``` @@ -305,7 +303,6 @@ Methods: - client.agents.auth.retrieve(id) -> AuthAgent - client.agents.auth.list(\*\*params) -> SyncOffsetPagination[AuthAgent] - client.agents.auth.delete(id) -> None -- client.agents.auth.reauth(id) -> ReauthResponse ### Invocations @@ -319,7 +316,6 @@ Methods: - client.agents.auth.invocations.create(\*\*params) -> AuthAgentInvocationCreateResponse - client.agents.auth.invocations.retrieve(invocation_id) -> AgentAuthInvocationResponse -- client.agents.auth.invocations.discover(invocation_id, \*\*params) -> AgentAuthDiscoverResponse - client.agents.auth.invocations.exchange(invocation_id, \*\*params) -> InvocationExchangeResponse - client.agents.auth.invocations.submit(invocation_id, \*\*params) -> AgentAuthSubmitResponse @@ -328,13 +324,19 @@ Methods: Types: ```python -from kernel.types import CreateCredentialRequest, Credential, UpdateCredentialRequest +from kernel.types import ( + CreateCredentialRequest, + Credential, + UpdateCredentialRequest, + CredentialTotpCodeResponse, +) ``` Methods: - client.credentials.create(\*\*params) -> Credential -- client.credentials.retrieve(id) -> Credential -- client.credentials.update(id, \*\*params) -> Credential +- client.credentials.retrieve(id_or_name) -> Credential +- client.credentials.update(id_or_name, \*\*params) -> Credential - client.credentials.list(\*\*params) -> SyncOffsetPagination[Credential] -- client.credentials.delete(id) -> None +- client.credentials.delete(id_or_name) -> None +- client.credentials.totp_code(id_or_name) -> CredentialTotpCodeResponse diff --git a/src/kernel/resources/agents/auth/auth.py b/src/kernel/resources/agents/auth/auth.py index 39f22fcb..f4a02767 100644 --- a/src/kernel/resources/agents/auth/auth.py +++ b/src/kernel/resources/agents/auth/auth.py @@ -4,7 +4,7 @@ import httpx -from ...._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given +from ...._types import Body, Omit, Query, Headers, NoneType, NotGiven, SequenceNotStr, omit, not_given from ...._utils import maybe_transform, async_maybe_transform from ...._compat import cached_property from .invocations import ( @@ -26,7 +26,6 @@ from ...._base_client import AsyncPaginator, make_request_options from ....types.agents import auth_list_params, auth_create_params from ....types.agents.auth_agent import AuthAgent -from ....types.agents.reauth_response import ReauthResponse __all__ = ["AuthResource", "AsyncAuthResource"] @@ -58,8 +57,9 @@ def with_streaming_response(self) -> AuthResourceWithStreamingResponse: def create( self, *, + domain: str, profile_name: str, - target_domain: str, + allowed_domains: SequenceNotStr[str] | Omit = omit, credential_name: str | Omit = omit, login_url: str | Omit = omit, proxy: auth_create_params.Proxy | Omit = omit, @@ -77,9 +77,13 @@ def create( invocation - use POST /agents/auth/invocations to start an auth flow. Args: + domain: Domain for authentication + profile_name: Name of the profile to use for this auth agent - target_domain: Target domain for authentication + allowed_domains: Additional domains that are valid for this auth agent's authentication flow + (besides the primary domain). Useful when login pages redirect to different + domains. credential_name: Optional name of an existing credential to use for this auth agent. If provided, the credential will be linked to the agent and its values will be used to @@ -102,8 +106,9 @@ def create( "/agents/auth", body=maybe_transform( { + "domain": domain, "profile_name": profile_name, - "target_domain": target_domain, + "allowed_domains": allowed_domains, "credential_name": credential_name, "login_url": login_url, "proxy": proxy, @@ -154,10 +159,10 @@ def retrieve( def list( self, *, + domain: str | Omit = omit, limit: int | Omit = omit, offset: int | Omit = omit, profile_name: str | Omit = omit, - target_domain: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -166,17 +171,17 @@ def list( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SyncOffsetPagination[AuthAgent]: """ - List auth agents with optional filters for profile_name and target_domain. + List auth agents with optional filters for profile_name and domain. Args: + domain: Filter by domain + limit: Maximum number of results to return offset: Number of results to skip profile_name: Filter by profile name - target_domain: Filter by target domain - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -195,10 +200,10 @@ def list( timeout=timeout, query=maybe_transform( { + "domain": domain, "limit": limit, "offset": offset, "profile_name": profile_name, - "target_domain": target_domain, }, auth_list_params.AuthListParams, ), @@ -245,42 +250,6 @@ def delete( cast_to=NoneType, ) - def reauth( - self, - id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ReauthResponse: - """ - Triggers automatic re-authentication for an auth agent using stored credentials. - Requires the auth agent to have a linked credential, stored selectors, and - login_url. Returns immediately with status indicating whether re-auth was - started. - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - return self._post( - f"/agents/auth/{id}/reauth", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ReauthResponse, - ) - class AsyncAuthResource(AsyncAPIResource): @cached_property @@ -309,8 +278,9 @@ def with_streaming_response(self) -> AsyncAuthResourceWithStreamingResponse: async def create( self, *, + domain: str, profile_name: str, - target_domain: str, + allowed_domains: SequenceNotStr[str] | Omit = omit, credential_name: str | Omit = omit, login_url: str | Omit = omit, proxy: auth_create_params.Proxy | Omit = omit, @@ -328,9 +298,13 @@ async def create( invocation - use POST /agents/auth/invocations to start an auth flow. Args: + domain: Domain for authentication + profile_name: Name of the profile to use for this auth agent - target_domain: Target domain for authentication + allowed_domains: Additional domains that are valid for this auth agent's authentication flow + (besides the primary domain). Useful when login pages redirect to different + domains. credential_name: Optional name of an existing credential to use for this auth agent. If provided, the credential will be linked to the agent and its values will be used to @@ -353,8 +327,9 @@ async def create( "/agents/auth", body=await async_maybe_transform( { + "domain": domain, "profile_name": profile_name, - "target_domain": target_domain, + "allowed_domains": allowed_domains, "credential_name": credential_name, "login_url": login_url, "proxy": proxy, @@ -405,10 +380,10 @@ async def retrieve( def list( self, *, + domain: str | Omit = omit, limit: int | Omit = omit, offset: int | Omit = omit, profile_name: str | Omit = omit, - target_domain: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -417,17 +392,17 @@ def list( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncPaginator[AuthAgent, AsyncOffsetPagination[AuthAgent]]: """ - List auth agents with optional filters for profile_name and target_domain. + List auth agents with optional filters for profile_name and domain. Args: + domain: Filter by domain + limit: Maximum number of results to return offset: Number of results to skip profile_name: Filter by profile name - target_domain: Filter by target domain - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -446,10 +421,10 @@ def list( timeout=timeout, query=maybe_transform( { + "domain": domain, "limit": limit, "offset": offset, "profile_name": profile_name, - "target_domain": target_domain, }, auth_list_params.AuthListParams, ), @@ -496,42 +471,6 @@ async def delete( cast_to=NoneType, ) - async def reauth( - self, - id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ReauthResponse: - """ - Triggers automatic re-authentication for an auth agent using stored credentials. - Requires the auth agent to have a linked credential, stored selectors, and - login_url. Returns immediately with status indicating whether re-auth was - started. - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - return await self._post( - f"/agents/auth/{id}/reauth", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ReauthResponse, - ) - class AuthResourceWithRawResponse: def __init__(self, auth: AuthResource) -> None: @@ -549,9 +488,6 @@ def __init__(self, auth: AuthResource) -> None: self.delete = to_raw_response_wrapper( auth.delete, ) - self.reauth = to_raw_response_wrapper( - auth.reauth, - ) @cached_property def invocations(self) -> InvocationsResourceWithRawResponse: @@ -574,9 +510,6 @@ def __init__(self, auth: AsyncAuthResource) -> None: self.delete = async_to_raw_response_wrapper( auth.delete, ) - self.reauth = async_to_raw_response_wrapper( - auth.reauth, - ) @cached_property def invocations(self) -> AsyncInvocationsResourceWithRawResponse: @@ -599,9 +532,6 @@ def __init__(self, auth: AuthResource) -> None: self.delete = to_streamed_response_wrapper( auth.delete, ) - self.reauth = to_streamed_response_wrapper( - auth.reauth, - ) @cached_property def invocations(self) -> InvocationsResourceWithStreamingResponse: @@ -624,9 +554,6 @@ def __init__(self, auth: AsyncAuthResource) -> None: self.delete = async_to_streamed_response_wrapper( auth.delete, ) - self.reauth = async_to_streamed_response_wrapper( - auth.reauth, - ) @cached_property def invocations(self) -> AsyncInvocationsResourceWithStreamingResponse: diff --git a/src/kernel/resources/agents/auth/invocations.py b/src/kernel/resources/agents/auth/invocations.py index f5e60533..34ab614d 100644 --- a/src/kernel/resources/agents/auth/invocations.py +++ b/src/kernel/resources/agents/auth/invocations.py @@ -2,12 +2,13 @@ from __future__ import annotations -from typing import Any, Dict, cast +from typing import Dict +from typing_extensions import overload import httpx from ...._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from ...._utils import maybe_transform, async_maybe_transform +from ...._utils import required_args, maybe_transform, async_maybe_transform from ...._compat import cached_property from ...._resource import SyncAPIResource, AsyncAPIResource from ...._response import ( @@ -17,14 +18,8 @@ async_to_streamed_response_wrapper, ) from ...._base_client import make_request_options -from ....types.agents.auth import ( - invocation_create_params, - invocation_submit_params, - invocation_discover_params, - invocation_exchange_params, -) +from ....types.agents.auth import invocation_create_params, invocation_submit_params, invocation_exchange_params from ....types.agents.agent_auth_submit_response import AgentAuthSubmitResponse -from ....types.agents.agent_auth_discover_response import AgentAuthDiscoverResponse from ....types.agents.agent_auth_invocation_response import AgentAuthInvocationResponse from ....types.agents.auth.invocation_exchange_response import InvocationExchangeResponse from ....types.agents.auth_agent_invocation_create_response import AuthAgentInvocationCreateResponse @@ -85,24 +80,19 @@ def create( timeout: Override the client-level default timeout for this request, in seconds """ - return cast( - AuthAgentInvocationCreateResponse, - self._post( - "/agents/auth/invocations", - body=maybe_transform( - { - "auth_agent_id": auth_agent_id, - "save_credential_as": save_credential_as, - }, - invocation_create_params.InvocationCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=cast( - Any, AuthAgentInvocationCreateResponse - ), # Union types cannot be passed in as arguments in the type system + return self._post( + "/agents/auth/invocations", + body=maybe_transform( + { + "auth_agent_id": auth_agent_id, + "save_credential_as": save_credential_as, + }, + invocation_create_params.InvocationCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), + cast_to=AuthAgentInvocationCreateResponse, ) def retrieve( @@ -116,9 +106,10 @@ def retrieve( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AgentAuthInvocationResponse: - """ - Returns invocation details including status, app_name, and target_domain. - Supports both API key and JWT (from exchange endpoint) authentication. + """Returns invocation details including status, app_name, and domain. + + Supports both + API key and JWT (from exchange endpoint) authentication. Args: extra_headers: Send extra headers @@ -139,26 +130,25 @@ def retrieve( cast_to=AgentAuthInvocationResponse, ) - def discover( + def exchange( self, invocation_id: str, *, - login_url: str | Omit = omit, + code: 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, - ) -> AgentAuthDiscoverResponse: - """ - Inspects the target site to detect logged-in state or discover required fields. - Returns 200 with success: true when fields are found, or 4xx/5xx for failures. - Supports both API key and JWT (from exchange endpoint) authentication. + ) -> InvocationExchangeResponse: + """Validates the handoff code and returns a JWT token for subsequent requests. + + No + authentication required (the handoff code serves as the credential). Args: - login_url: Optional login page URL. If provided, will override the stored login URL for - this discovery invocation and skip Phase 1 discovery. + code: Handoff code from start endpoint extra_headers: Send extra headers @@ -171,33 +161,35 @@ def discover( if not invocation_id: raise ValueError(f"Expected a non-empty value for `invocation_id` but received {invocation_id!r}") return self._post( - f"/agents/auth/invocations/{invocation_id}/discover", - body=maybe_transform({"login_url": login_url}, invocation_discover_params.InvocationDiscoverParams), + f"/agents/auth/invocations/{invocation_id}/exchange", + body=maybe_transform({"code": code}, invocation_exchange_params.InvocationExchangeParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=AgentAuthDiscoverResponse, + cast_to=InvocationExchangeResponse, ) - def exchange( + @overload + def submit( self, invocation_id: str, *, - code: str, + field_values: Dict[str, 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, - ) -> InvocationExchangeResponse: - """Validates the handoff code and returns a JWT token for subsequent requests. + ) -> AgentAuthSubmitResponse: + """Submits field values for the discovered login form. - No - authentication required (the handoff code serves as the credential). + Returns immediately after + submission is accepted. Poll the invocation endpoint to track progress and get + results. Args: - code: Handoff code from start endpoint + field_values: Values for the discovered login fields extra_headers: Send extra headers @@ -207,22 +199,14 @@ def exchange( timeout: Override the client-level default timeout for this request, in seconds """ - if not invocation_id: - raise ValueError(f"Expected a non-empty value for `invocation_id` but received {invocation_id!r}") - return self._post( - f"/agents/auth/invocations/{invocation_id}/exchange", - body=maybe_transform({"code": code}, invocation_exchange_params.InvocationExchangeParams), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=InvocationExchangeResponse, - ) + ... + @overload def submit( self, invocation_id: str, *, - field_values: Dict[str, str], + sso_button: 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, @@ -230,13 +214,14 @@ def submit( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AgentAuthSubmitResponse: - """ - Submits field values for the discovered login form and may return additional - auth fields or success. Supports both API key and JWT (from exchange endpoint) - authentication. + """Submits field values for the discovered login form. + + Returns immediately after + submission is accepted. Poll the invocation endpoint to track progress and get + results. Args: - field_values: Values for the discovered login fields + sso_button: Selector of SSO button to click extra_headers: Send extra headers @@ -246,11 +231,33 @@ def submit( timeout: Override the client-level default timeout for this request, in seconds """ + ... + + @required_args(["field_values"], ["sso_button"]) + def submit( + self, + invocation_id: str, + *, + field_values: Dict[str, str] | Omit = omit, + sso_button: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AgentAuthSubmitResponse: if not invocation_id: raise ValueError(f"Expected a non-empty value for `invocation_id` but received {invocation_id!r}") return self._post( f"/agents/auth/invocations/{invocation_id}/submit", - body=maybe_transform({"field_values": field_values}, invocation_submit_params.InvocationSubmitParams), + body=maybe_transform( + { + "field_values": field_values, + "sso_button": sso_button, + }, + invocation_submit_params.InvocationSubmitParams, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -311,24 +318,19 @@ async def create( timeout: Override the client-level default timeout for this request, in seconds """ - return cast( - AuthAgentInvocationCreateResponse, - await self._post( - "/agents/auth/invocations", - body=await async_maybe_transform( - { - "auth_agent_id": auth_agent_id, - "save_credential_as": save_credential_as, - }, - invocation_create_params.InvocationCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=cast( - Any, AuthAgentInvocationCreateResponse - ), # Union types cannot be passed in as arguments in the type system + return await self._post( + "/agents/auth/invocations", + body=await async_maybe_transform( + { + "auth_agent_id": auth_agent_id, + "save_credential_as": save_credential_as, + }, + invocation_create_params.InvocationCreateParams, ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AuthAgentInvocationCreateResponse, ) async def retrieve( @@ -342,9 +344,10 @@ async def retrieve( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AgentAuthInvocationResponse: - """ - Returns invocation details including status, app_name, and target_domain. - Supports both API key and JWT (from exchange endpoint) authentication. + """Returns invocation details including status, app_name, and domain. + + Supports both + API key and JWT (from exchange endpoint) authentication. Args: extra_headers: Send extra headers @@ -365,26 +368,25 @@ async def retrieve( cast_to=AgentAuthInvocationResponse, ) - async def discover( + async def exchange( self, invocation_id: str, *, - login_url: str | Omit = omit, + code: 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, - ) -> AgentAuthDiscoverResponse: - """ - Inspects the target site to detect logged-in state or discover required fields. - Returns 200 with success: true when fields are found, or 4xx/5xx for failures. - Supports both API key and JWT (from exchange endpoint) authentication. + ) -> InvocationExchangeResponse: + """Validates the handoff code and returns a JWT token for subsequent requests. + + No + authentication required (the handoff code serves as the credential). Args: - login_url: Optional login page URL. If provided, will override the stored login URL for - this discovery invocation and skip Phase 1 discovery. + code: Handoff code from start endpoint extra_headers: Send extra headers @@ -397,35 +399,35 @@ async def discover( if not invocation_id: raise ValueError(f"Expected a non-empty value for `invocation_id` but received {invocation_id!r}") return await self._post( - f"/agents/auth/invocations/{invocation_id}/discover", - body=await async_maybe_transform( - {"login_url": login_url}, invocation_discover_params.InvocationDiscoverParams - ), + f"/agents/auth/invocations/{invocation_id}/exchange", + body=await async_maybe_transform({"code": code}, invocation_exchange_params.InvocationExchangeParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=AgentAuthDiscoverResponse, + cast_to=InvocationExchangeResponse, ) - async def exchange( + @overload + async def submit( self, invocation_id: str, *, - code: str, + field_values: Dict[str, 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, - ) -> InvocationExchangeResponse: - """Validates the handoff code and returns a JWT token for subsequent requests. + ) -> AgentAuthSubmitResponse: + """Submits field values for the discovered login form. - No - authentication required (the handoff code serves as the credential). + Returns immediately after + submission is accepted. Poll the invocation endpoint to track progress and get + results. Args: - code: Handoff code from start endpoint + field_values: Values for the discovered login fields extra_headers: Send extra headers @@ -435,22 +437,14 @@ async def exchange( timeout: Override the client-level default timeout for this request, in seconds """ - if not invocation_id: - raise ValueError(f"Expected a non-empty value for `invocation_id` but received {invocation_id!r}") - return await self._post( - f"/agents/auth/invocations/{invocation_id}/exchange", - body=await async_maybe_transform({"code": code}, invocation_exchange_params.InvocationExchangeParams), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=InvocationExchangeResponse, - ) + ... + @overload async def submit( self, invocation_id: str, *, - field_values: Dict[str, str], + sso_button: 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, @@ -458,13 +452,14 @@ async def submit( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AgentAuthSubmitResponse: - """ - Submits field values for the discovered login form and may return additional - auth fields or success. Supports both API key and JWT (from exchange endpoint) - authentication. + """Submits field values for the discovered login form. + + Returns immediately after + submission is accepted. Poll the invocation endpoint to track progress and get + results. Args: - field_values: Values for the discovered login fields + sso_button: Selector of SSO button to click extra_headers: Send extra headers @@ -474,12 +469,32 @@ async def submit( timeout: Override the client-level default timeout for this request, in seconds """ + ... + + @required_args(["field_values"], ["sso_button"]) + async def submit( + self, + invocation_id: str, + *, + field_values: Dict[str, str] | Omit = omit, + sso_button: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AgentAuthSubmitResponse: if not invocation_id: raise ValueError(f"Expected a non-empty value for `invocation_id` but received {invocation_id!r}") return await self._post( f"/agents/auth/invocations/{invocation_id}/submit", body=await async_maybe_transform( - {"field_values": field_values}, invocation_submit_params.InvocationSubmitParams + { + "field_values": field_values, + "sso_button": sso_button, + }, + invocation_submit_params.InvocationSubmitParams, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -498,9 +513,6 @@ def __init__(self, invocations: InvocationsResource) -> None: self.retrieve = to_raw_response_wrapper( invocations.retrieve, ) - self.discover = to_raw_response_wrapper( - invocations.discover, - ) self.exchange = to_raw_response_wrapper( invocations.exchange, ) @@ -519,9 +531,6 @@ def __init__(self, invocations: AsyncInvocationsResource) -> None: self.retrieve = async_to_raw_response_wrapper( invocations.retrieve, ) - self.discover = async_to_raw_response_wrapper( - invocations.discover, - ) self.exchange = async_to_raw_response_wrapper( invocations.exchange, ) @@ -540,9 +549,6 @@ def __init__(self, invocations: InvocationsResource) -> None: self.retrieve = to_streamed_response_wrapper( invocations.retrieve, ) - self.discover = to_streamed_response_wrapper( - invocations.discover, - ) self.exchange = to_streamed_response_wrapper( invocations.exchange, ) @@ -561,9 +567,6 @@ def __init__(self, invocations: AsyncInvocationsResource) -> None: self.retrieve = async_to_streamed_response_wrapper( invocations.retrieve, ) - self.discover = async_to_streamed_response_wrapper( - invocations.discover, - ) self.exchange = async_to_streamed_response_wrapper( invocations.exchange, ) diff --git a/src/kernel/resources/credentials.py b/src/kernel/resources/credentials.py index 91dafce8..85e0c8a0 100644 --- a/src/kernel/resources/credentials.py +++ b/src/kernel/resources/credentials.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Dict +from typing import Dict, Optional import httpx @@ -20,6 +20,7 @@ from ..pagination import SyncOffsetPagination, AsyncOffsetPagination from .._base_client import AsyncPaginator, make_request_options from ..types.credential import Credential +from ..types.credential_totp_code_response import CredentialTotpCodeResponse __all__ = ["CredentialsResource", "AsyncCredentialsResource"] @@ -50,6 +51,8 @@ def create( domain: str, name: str, values: Dict[str, str], + sso_provider: str | Omit = omit, + totp_secret: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -57,10 +60,8 @@ def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Credential: - """Create a new credential for storing login information. - - Values are encrypted at - rest. + """ + Create a new credential for storing login information. Args: domain: Target domain this credential is for @@ -69,6 +70,14 @@ def create( values: Field name to value mapping (e.g., username, password) + sso_provider: If set, indicates this credential should be used with the specified SSO provider + (e.g., google, github, microsoft). When the target site has a matching SSO + button, it will be clicked first before filling credential values on the + identity provider's login page. + + totp_secret: Base32-encoded TOTP secret for generating one-time passwords. Used for automatic + 2FA during login. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -84,6 +93,8 @@ def create( "domain": domain, "name": name, "values": values, + "sso_provider": sso_provider, + "totp_secret": totp_secret, }, credential_create_params.CredentialCreateParams, ), @@ -95,7 +106,7 @@ def create( def retrieve( self, - id: str, + id_or_name: 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. @@ -104,7 +115,7 @@ def retrieve( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Credential: - """Retrieve a credential by its ID. + """Retrieve a credential by its ID or name. Credential values are not returned. @@ -117,10 +128,10 @@ def retrieve( timeout: Override the client-level default timeout for this request, in seconds """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return self._get( - f"/credentials/{id}", + f"/credentials/{id_or_name}", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -129,9 +140,11 @@ def retrieve( def update( self, - id: str, + id_or_name: str, *, name: str | Omit = omit, + sso_provider: Optional[str] | Omit = omit, + totp_secret: str | Omit = omit, values: Dict[str, str] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -142,13 +155,20 @@ def update( ) -> Credential: """Update a credential's name or values. - Values are encrypted at rest. + When values are provided, they are merged + with existing values (new keys are added, existing keys are overwritten). Args: name: New name for the credential - values: Field name to value mapping (e.g., username, password). Replaces all existing - values. + sso_provider: If set, indicates this credential should be used with the specified SSO + provider. Set to empty string or null to remove. + + totp_secret: Base32-encoded TOTP secret for generating one-time passwords. Spaces and + formatting are automatically normalized. Set to empty string to remove. + + values: Field name to value mapping. Values are merged with existing values (new keys + added, existing keys overwritten). extra_headers: Send extra headers @@ -158,13 +178,15 @@ def update( timeout: Override the client-level default timeout for this request, in seconds """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return self._patch( - f"/credentials/{id}", + f"/credentials/{id_or_name}", body=maybe_transform( { "name": name, + "sso_provider": sso_provider, + "totp_secret": totp_secret, "values": values, }, credential_update_params.CredentialUpdateParams, @@ -230,7 +252,7 @@ def list( def delete( self, - id: str, + id_or_name: 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. @@ -240,7 +262,7 @@ def delete( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ - Delete a credential by its ID. + Delete a credential by its ID or name. Args: extra_headers: Send extra headers @@ -251,17 +273,52 @@ def delete( timeout: Override the client-level default timeout for this request, in seconds """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( - f"/credentials/{id}", + f"/credentials/{id_or_name}", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=NoneType, ) + def totp_code( + self, + id_or_name: 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, + ) -> CredentialTotpCodeResponse: + """ + Returns the current 6-digit TOTP code for a credential with a configured + totp_secret. Use this to complete 2FA setup on sites or when you need a fresh + code. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") + return self._get( + f"/credentials/{id_or_name}/totp-code", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CredentialTotpCodeResponse, + ) + class AsyncCredentialsResource(AsyncAPIResource): @cached_property @@ -289,6 +346,8 @@ async def create( domain: str, name: str, values: Dict[str, str], + sso_provider: str | Omit = omit, + totp_secret: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -296,10 +355,8 @@ async def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Credential: - """Create a new credential for storing login information. - - Values are encrypted at - rest. + """ + Create a new credential for storing login information. Args: domain: Target domain this credential is for @@ -308,6 +365,14 @@ async def create( values: Field name to value mapping (e.g., username, password) + sso_provider: If set, indicates this credential should be used with the specified SSO provider + (e.g., google, github, microsoft). When the target site has a matching SSO + button, it will be clicked first before filling credential values on the + identity provider's login page. + + totp_secret: Base32-encoded TOTP secret for generating one-time passwords. Used for automatic + 2FA during login. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -323,6 +388,8 @@ async def create( "domain": domain, "name": name, "values": values, + "sso_provider": sso_provider, + "totp_secret": totp_secret, }, credential_create_params.CredentialCreateParams, ), @@ -334,7 +401,7 @@ async def create( async def retrieve( self, - id: str, + id_or_name: 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. @@ -343,7 +410,7 @@ async def retrieve( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Credential: - """Retrieve a credential by its ID. + """Retrieve a credential by its ID or name. Credential values are not returned. @@ -356,10 +423,10 @@ async def retrieve( timeout: Override the client-level default timeout for this request, in seconds """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return await self._get( - f"/credentials/{id}", + f"/credentials/{id_or_name}", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -368,9 +435,11 @@ async def retrieve( async def update( self, - id: str, + id_or_name: str, *, name: str | Omit = omit, + sso_provider: Optional[str] | Omit = omit, + totp_secret: str | Omit = omit, values: Dict[str, str] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -381,13 +450,20 @@ async def update( ) -> Credential: """Update a credential's name or values. - Values are encrypted at rest. + When values are provided, they are merged + with existing values (new keys are added, existing keys are overwritten). Args: name: New name for the credential - values: Field name to value mapping (e.g., username, password). Replaces all existing - values. + sso_provider: If set, indicates this credential should be used with the specified SSO + provider. Set to empty string or null to remove. + + totp_secret: Base32-encoded TOTP secret for generating one-time passwords. Spaces and + formatting are automatically normalized. Set to empty string to remove. + + values: Field name to value mapping. Values are merged with existing values (new keys + added, existing keys overwritten). extra_headers: Send extra headers @@ -397,13 +473,15 @@ async def update( timeout: Override the client-level default timeout for this request, in seconds """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return await self._patch( - f"/credentials/{id}", + f"/credentials/{id_or_name}", body=await async_maybe_transform( { "name": name, + "sso_provider": sso_provider, + "totp_secret": totp_secret, "values": values, }, credential_update_params.CredentialUpdateParams, @@ -469,7 +547,7 @@ def list( async def delete( self, - id: str, + id_or_name: 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. @@ -479,7 +557,7 @@ async def delete( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ - Delete a credential by its ID. + Delete a credential by its ID or name. Args: extra_headers: Send extra headers @@ -490,17 +568,52 @@ async def delete( timeout: Override the client-level default timeout for this request, in seconds """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( - f"/credentials/{id}", + f"/credentials/{id_or_name}", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=NoneType, ) + async def totp_code( + self, + id_or_name: 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, + ) -> CredentialTotpCodeResponse: + """ + Returns the current 6-digit TOTP code for a credential with a configured + totp_secret. Use this to complete 2FA setup on sites or when you need a fresh + code. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") + return await self._get( + f"/credentials/{id_or_name}/totp-code", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CredentialTotpCodeResponse, + ) + class CredentialsResourceWithRawResponse: def __init__(self, credentials: CredentialsResource) -> None: @@ -521,6 +634,9 @@ def __init__(self, credentials: CredentialsResource) -> None: self.delete = to_raw_response_wrapper( credentials.delete, ) + self.totp_code = to_raw_response_wrapper( + credentials.totp_code, + ) class AsyncCredentialsResourceWithRawResponse: @@ -542,6 +658,9 @@ def __init__(self, credentials: AsyncCredentialsResource) -> None: self.delete = async_to_raw_response_wrapper( credentials.delete, ) + self.totp_code = async_to_raw_response_wrapper( + credentials.totp_code, + ) class CredentialsResourceWithStreamingResponse: @@ -563,6 +682,9 @@ def __init__(self, credentials: CredentialsResource) -> None: self.delete = to_streamed_response_wrapper( credentials.delete, ) + self.totp_code = to_streamed_response_wrapper( + credentials.totp_code, + ) class AsyncCredentialsResourceWithStreamingResponse: @@ -584,3 +706,6 @@ def __init__(self, credentials: AsyncCredentialsResource) -> None: self.delete = async_to_streamed_response_wrapper( credentials.delete, ) + self.totp_code = async_to_streamed_response_wrapper( + credentials.totp_code, + ) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 1748bf22..0665e536 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -64,6 +64,7 @@ from .deployment_retrieve_response import DeploymentRetrieveResponse as DeploymentRetrieveResponse from .invocation_retrieve_response import InvocationRetrieveResponse as InvocationRetrieveResponse from .browser_pool_acquire_response import BrowserPoolAcquireResponse as BrowserPoolAcquireResponse +from .credential_totp_code_response import CredentialTotpCodeResponse as CredentialTotpCodeResponse from .browser_load_extensions_params import BrowserLoadExtensionsParams as BrowserLoadExtensionsParams from .extension_download_from_chrome_store_params import ( ExtensionDownloadFromChromeStoreParams as ExtensionDownloadFromChromeStoreParams, diff --git a/src/kernel/types/agents/__init__.py b/src/kernel/types/agents/__init__.py index e2c3bb0c..2ecdef65 100644 --- a/src/kernel/types/agents/__init__.py +++ b/src/kernel/types/agents/__init__.py @@ -3,12 +3,10 @@ from __future__ import annotations from .auth_agent import AuthAgent as AuthAgent -from .reauth_response import ReauthResponse as ReauthResponse from .auth_list_params import AuthListParams as AuthListParams from .discovered_field import DiscoveredField as DiscoveredField from .auth_create_params import AuthCreateParams as AuthCreateParams from .agent_auth_submit_response import AgentAuthSubmitResponse as AgentAuthSubmitResponse -from .agent_auth_discover_response import AgentAuthDiscoverResponse as AgentAuthDiscoverResponse from .agent_auth_invocation_response import AgentAuthInvocationResponse as AgentAuthInvocationResponse from .auth_agent_invocation_create_response import ( AuthAgentInvocationCreateResponse as AuthAgentInvocationCreateResponse, diff --git a/src/kernel/types/agents/agent_auth_discover_response.py b/src/kernel/types/agents/agent_auth_discover_response.py deleted file mode 100644 index 5e411dcb..00000000 --- a/src/kernel/types/agents/agent_auth_discover_response.py +++ /dev/null @@ -1,30 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Optional - -from ..._models import BaseModel -from .discovered_field import DiscoveredField - -__all__ = ["AgentAuthDiscoverResponse"] - - -class AgentAuthDiscoverResponse(BaseModel): - """Response from discover endpoint matching AuthBlueprint schema""" - - success: bool - """Whether discovery succeeded""" - - error_message: Optional[str] = None - """Error message if discovery failed""" - - fields: Optional[List[DiscoveredField]] = None - """Discovered form fields (present when success is true)""" - - logged_in: Optional[bool] = None - """Whether user is already logged in""" - - login_url: Optional[str] = None - """URL of the discovered login page""" - - page_title: Optional[str] = None - """Title of the login page""" diff --git a/src/kernel/types/agents/agent_auth_invocation_response.py b/src/kernel/types/agents/agent_auth_invocation_response.py index 3ddfe8e2..42b54a4c 100644 --- a/src/kernel/types/agents/agent_auth_invocation_response.py +++ b/src/kernel/types/agents/agent_auth_invocation_response.py @@ -1,11 +1,26 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from typing import List, Optional from datetime import datetime from typing_extensions import Literal from ..._models import BaseModel +from .discovered_field import DiscoveredField -__all__ = ["AgentAuthInvocationResponse"] +__all__ = ["AgentAuthInvocationResponse", "PendingSSOButton"] + + +class PendingSSOButton(BaseModel): + """An SSO button for signing in with an external identity provider""" + + label: str + """Visible button text""" + + provider: str + """Identity provider name""" + + selector: str + """XPath selector for the button""" class AgentAuthInvocationResponse(BaseModel): @@ -14,14 +29,47 @@ class AgentAuthInvocationResponse(BaseModel): app_name: str """App name (org name at time of invocation creation)""" + domain: str + """Domain for authentication""" + expires_at: datetime """When the handoff code expires""" - status: Literal["IN_PROGRESS", "SUCCESS", "EXPIRED", "CANCELED"] + status: Literal["IN_PROGRESS", "SUCCESS", "EXPIRED", "CANCELED", "FAILED"] """Invocation status""" - step: Literal["initialized", "discovering", "awaiting_input", "submitting", "completed", "expired"] + step: Literal[ + "initialized", "discovering", "awaiting_input", "awaiting_external_action", "submitting", "completed", "expired" + ] """Current step in the invocation workflow""" - target_domain: str - """Target domain for authentication""" + type: Literal["login", "auto_login", "reauth"] + """The invocation type: + + - login: First-time authentication + - reauth: Re-authentication for previously authenticated agents + - auto_login: Legacy type (no longer created, kept for backward compatibility) + """ + + error_message: Optional[str] = None + """Error message explaining why the invocation failed (present when status=FAILED)""" + + external_action_message: Optional[str] = None + """ + Instructions for user when external action is required (present when + step=awaiting_external_action) + """ + + live_view_url: Optional[str] = None + """Browser live view URL for debugging the invocation""" + + pending_fields: Optional[List[DiscoveredField]] = None + """Fields currently awaiting input (present when step=awaiting_input)""" + + pending_sso_buttons: Optional[List[PendingSSOButton]] = None + """SSO buttons available on the page (present when step=awaiting_input)""" + + submitted_fields: Optional[List[str]] = None + """ + Names of fields that have been submitted (present when step=submitting or later) + """ diff --git a/src/kernel/types/agents/agent_auth_submit_response.py b/src/kernel/types/agents/agent_auth_submit_response.py index 5ca9578c..8cb0df14 100644 --- a/src/kernel/types/agents/agent_auth_submit_response.py +++ b/src/kernel/types/agents/agent_auth_submit_response.py @@ -1,36 +1,14 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List, Optional - from ..._models import BaseModel -from .discovered_field import DiscoveredField __all__ = ["AgentAuthSubmitResponse"] class AgentAuthSubmitResponse(BaseModel): - """Response from submit endpoint matching SubmitResult schema""" - - success: bool - """Whether submission succeeded""" - - additional_fields: Optional[List[DiscoveredField]] = None """ - Additional fields needed (e.g., OTP) - present when needs_additional_auth is - true + Response from submit endpoint - returns immediately after submission is accepted """ - app_name: Optional[str] = None - """App name (only present when logged_in is true)""" - - error_message: Optional[str] = None - """Error message if submission failed""" - - logged_in: Optional[bool] = None - """Whether user is now logged in""" - - needs_additional_auth: Optional[bool] = None - """Whether additional authentication fields are needed""" - - target_domain: Optional[str] = None - """Target domain (only present when logged_in is true)""" + accepted: bool + """Whether the submission was accepted for processing""" diff --git a/src/kernel/types/agents/auth/__init__.py b/src/kernel/types/agents/auth/__init__.py index 02968833..41e8ba8c 100644 --- a/src/kernel/types/agents/auth/__init__.py +++ b/src/kernel/types/agents/auth/__init__.py @@ -4,6 +4,5 @@ from .invocation_create_params import InvocationCreateParams as InvocationCreateParams from .invocation_submit_params import InvocationSubmitParams as InvocationSubmitParams -from .invocation_discover_params import InvocationDiscoverParams as InvocationDiscoverParams from .invocation_exchange_params import InvocationExchangeParams as InvocationExchangeParams from .invocation_exchange_response import InvocationExchangeResponse as InvocationExchangeResponse diff --git a/src/kernel/types/agents/auth/invocation_discover_params.py b/src/kernel/types/agents/auth/invocation_discover_params.py deleted file mode 100644 index aa03f0cd..00000000 --- a/src/kernel/types/agents/auth/invocation_discover_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 TypedDict - -__all__ = ["InvocationDiscoverParams"] - - -class InvocationDiscoverParams(TypedDict, total=False): - login_url: str - """Optional login page URL. - - If provided, will override the stored login URL for this discovery invocation - and skip Phase 1 discovery. - """ diff --git a/src/kernel/types/agents/auth/invocation_submit_params.py b/src/kernel/types/agents/auth/invocation_submit_params.py index be92e7de..ad9f9c18 100644 --- a/src/kernel/types/agents/auth/invocation_submit_params.py +++ b/src/kernel/types/agents/auth/invocation_submit_params.py @@ -2,12 +2,20 @@ from __future__ import annotations -from typing import Dict -from typing_extensions import Required, TypedDict +from typing import Dict, Union +from typing_extensions import Required, TypeAlias, TypedDict -__all__ = ["InvocationSubmitParams"] +__all__ = ["InvocationSubmitParams", "Variant0", "Variant1"] -class InvocationSubmitParams(TypedDict, total=False): +class Variant0(TypedDict, total=False): field_values: Required[Dict[str, str]] """Values for the discovered login fields""" + + +class Variant1(TypedDict, total=False): + sso_button: Required[str] + """Selector of SSO button to click""" + + +InvocationSubmitParams: TypeAlias = Union[Variant0, Variant1] diff --git a/src/kernel/types/agents/auth_agent.py b/src/kernel/types/agents/auth_agent.py index 50f91499..33fc46b2 100644 --- a/src/kernel/types/agents/auth_agent.py +++ b/src/kernel/types/agents/auth_agent.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional +from typing import List, Optional from datetime import datetime from typing_extensions import Literal @@ -26,6 +26,13 @@ class AuthAgent(BaseModel): status: Literal["AUTHENTICATED", "NEEDS_AUTH"] """Current authentication status of the managed profile""" + allowed_domains: Optional[List[str]] = None + """ + Additional domains that are valid for this auth agent's authentication flow + (besides the primary domain). Useful when login pages redirect to different + domains. + """ + can_reauth: Optional[bool] = None """ Whether automatic re-authentication is possible (has credential_id, selectors, diff --git a/src/kernel/types/agents/auth_agent_invocation_create_response.py b/src/kernel/types/agents/auth_agent_invocation_create_response.py index b2a5e20c..6027f4d8 100644 --- a/src/kernel/types/agents/auth_agent_invocation_create_response.py +++ b/src/kernel/types/agents/auth_agent_invocation_create_response.py @@ -1,24 +1,15 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Union from datetime import datetime -from typing_extensions import Literal, Annotated, TypeAlias +from typing_extensions import Literal -from ..._utils import PropertyInfo from ..._models import BaseModel -__all__ = ["AuthAgentInvocationCreateResponse", "AuthAgentAlreadyAuthenticated", "AuthAgentInvocationCreated"] +__all__ = ["AuthAgentInvocationCreateResponse"] -class AuthAgentAlreadyAuthenticated(BaseModel): - """Response when the agent is already authenticated.""" - - status: Literal["ALREADY_AUTHENTICATED"] - """Indicates the agent is already authenticated and no invocation was created.""" - - -class AuthAgentInvocationCreated(BaseModel): - """Response when a new invocation was created.""" +class AuthAgentInvocationCreateResponse(BaseModel): + """Response from creating an invocation. Always returns an invocation_id.""" expires_at: datetime """When the handoff code expires.""" @@ -32,10 +23,10 @@ class AuthAgentInvocationCreated(BaseModel): invocation_id: str """Unique identifier for the invocation.""" - status: Literal["INVOCATION_CREATED"] - """Indicates an invocation was created.""" - + type: Literal["login", "auto_login", "reauth"] + """The invocation type: -AuthAgentInvocationCreateResponse: TypeAlias = Annotated[ - Union[AuthAgentAlreadyAuthenticated, AuthAgentInvocationCreated], PropertyInfo(discriminator="status") -] + - login: First-time authentication + - reauth: Re-authentication for previously authenticated agents + - auto_login: Legacy type (no longer created, kept for backward compatibility) + """ diff --git a/src/kernel/types/agents/auth_create_params.py b/src/kernel/types/agents/auth_create_params.py index 7cf7665f..b792d566 100644 --- a/src/kernel/types/agents/auth_create_params.py +++ b/src/kernel/types/agents/auth_create_params.py @@ -4,15 +4,24 @@ from typing_extensions import Required, TypedDict +from ..._types import SequenceNotStr + __all__ = ["AuthCreateParams", "Proxy"] class AuthCreateParams(TypedDict, total=False): + domain: Required[str] + """Domain for authentication""" + profile_name: Required[str] """Name of the profile to use for this auth agent""" - target_domain: Required[str] - """Target domain for authentication""" + allowed_domains: SequenceNotStr[str] + """ + Additional domains that are valid for this auth agent's authentication flow + (besides the primary domain). Useful when login pages redirect to different + domains. + """ credential_name: str """Optional name of an existing credential to use for this auth agent. diff --git a/src/kernel/types/agents/auth_list_params.py b/src/kernel/types/agents/auth_list_params.py index a4b2ffcb..52d53375 100644 --- a/src/kernel/types/agents/auth_list_params.py +++ b/src/kernel/types/agents/auth_list_params.py @@ -8,6 +8,9 @@ class AuthListParams(TypedDict, total=False): + domain: str + """Filter by domain""" + limit: int """Maximum number of results to return""" @@ -16,6 +19,3 @@ class AuthListParams(TypedDict, total=False): profile_name: str """Filter by profile name""" - - target_domain: str - """Filter by target domain""" diff --git a/src/kernel/types/agents/discovered_field.py b/src/kernel/types/agents/discovered_field.py index 0c6715c1..72ac2949 100644 --- a/src/kernel/types/agents/discovered_field.py +++ b/src/kernel/types/agents/discovered_field.py @@ -20,7 +20,7 @@ class DiscoveredField(BaseModel): selector: str """CSS selector for the field""" - type: Literal["text", "email", "password", "tel", "number", "url", "code"] + type: Literal["text", "email", "password", "tel", "number", "url", "code", "totp"] """Field type""" placeholder: Optional[str] = None diff --git a/src/kernel/types/agents/reauth_response.py b/src/kernel/types/agents/reauth_response.py deleted file mode 100644 index 4fbf0e47..00000000 --- a/src/kernel/types/agents/reauth_response.py +++ /dev/null @@ -1,21 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional -from typing_extensions import Literal - -from ..._models import BaseModel - -__all__ = ["ReauthResponse"] - - -class ReauthResponse(BaseModel): - """Response from triggering re-authentication""" - - status: Literal["REAUTH_STARTED", "ALREADY_AUTHENTICATED", "CANNOT_REAUTH"] - """Result of the re-authentication attempt""" - - invocation_id: Optional[str] = None - """ID of the re-auth invocation if one was created""" - - message: Optional[str] = None - """Human-readable description of the result""" diff --git a/src/kernel/types/credential.py b/src/kernel/types/credential.py index 30def062..8ae733bc 100644 --- a/src/kernel/types/credential.py +++ b/src/kernel/types/credential.py @@ -1,5 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from typing import Optional from datetime import datetime from .._models import BaseModel @@ -24,3 +25,23 @@ class Credential(BaseModel): updated_at: datetime """When the credential was last updated""" + + has_totp_secret: Optional[bool] = None + """Whether this credential has a TOTP secret configured for automatic 2FA""" + + sso_provider: Optional[str] = None + """ + If set, indicates this credential should be used with the specified SSO provider + (e.g., google, github, microsoft). When the target site has a matching SSO + button, it will be clicked first before filling credential values on the + identity provider's login page. + """ + + totp_code: Optional[str] = None + """Current 6-digit TOTP code. + + Only included in create/update responses when totp_secret was just set. + """ + + totp_code_expires_at: Optional[datetime] = None + """When the totp_code expires. Only included when totp_code is present.""" diff --git a/src/kernel/types/credential_create_params.py b/src/kernel/types/credential_create_params.py index 3f7b2d90..94964b9d 100644 --- a/src/kernel/types/credential_create_params.py +++ b/src/kernel/types/credential_create_params.py @@ -17,3 +17,17 @@ class CredentialCreateParams(TypedDict, total=False): values: Required[Dict[str, str]] """Field name to value mapping (e.g., username, password)""" + + sso_provider: str + """ + If set, indicates this credential should be used with the specified SSO provider + (e.g., google, github, microsoft). When the target site has a matching SSO + button, it will be clicked first before filling credential values on the + identity provider's login page. + """ + + totp_secret: str + """Base32-encoded TOTP secret for generating one-time passwords. + + Used for automatic 2FA during login. + """ diff --git a/src/kernel/types/credential_totp_code_response.py b/src/kernel/types/credential_totp_code_response.py new file mode 100644 index 00000000..670f4e7c --- /dev/null +++ b/src/kernel/types/credential_totp_code_response.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from datetime import datetime + +from .._models import BaseModel + +__all__ = ["CredentialTotpCodeResponse"] + + +class CredentialTotpCodeResponse(BaseModel): + code: str + """Current 6-digit TOTP code""" + + expires_at: datetime + """When this code expires (ISO 8601 timestamp)""" diff --git a/src/kernel/types/credential_update_params.py b/src/kernel/types/credential_update_params.py index ffc0c1cc..c42209e4 100644 --- a/src/kernel/types/credential_update_params.py +++ b/src/kernel/types/credential_update_params.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Dict +from typing import Dict, Optional from typing_extensions import TypedDict __all__ = ["CredentialUpdateParams"] @@ -12,8 +12,23 @@ class CredentialUpdateParams(TypedDict, total=False): name: str """New name for the credential""" + sso_provider: Optional[str] + """If set, indicates this credential should be used with the specified SSO + provider. + + Set to empty string or null to remove. + """ + + totp_secret: str + """Base32-encoded TOTP secret for generating one-time passwords. + + Spaces and formatting are automatically normalized. Set to empty string to + remove. + """ + values: Dict[str, str] - """Field name to value mapping (e.g., username, password). + """Field name to value mapping. - Replaces all existing values. + Values are merged with existing values (new keys added, existing keys + overwritten). """ diff --git a/tests/api_resources/agents/auth/test_invocations.py b/tests/api_resources/agents/auth/test_invocations.py index eef21a9b..1bae66da 100644 --- a/tests/api_resources/agents/auth/test_invocations.py +++ b/tests/api_resources/agents/auth/test_invocations.py @@ -9,12 +9,7 @@ from kernel import Kernel, AsyncKernel from tests.utils import assert_matches_type -from kernel.types.agents import ( - AgentAuthSubmitResponse, - AgentAuthDiscoverResponse, - AgentAuthInvocationResponse, - AuthAgentInvocationCreateResponse, -) +from kernel.types.agents import AgentAuthSubmitResponse, AgentAuthInvocationResponse, AuthAgentInvocationCreateResponse from kernel.types.agents.auth import ( InvocationExchangeResponse, ) @@ -110,57 +105,6 @@ def test_path_params_retrieve(self, client: Kernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_discover(self, client: Kernel) -> None: - invocation = client.agents.auth.invocations.discover( - invocation_id="invocation_id", - ) - assert_matches_type(AgentAuthDiscoverResponse, invocation, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_discover_with_all_params(self, client: Kernel) -> None: - invocation = client.agents.auth.invocations.discover( - invocation_id="invocation_id", - login_url="https://doordash.com/account/login", - ) - assert_matches_type(AgentAuthDiscoverResponse, invocation, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_discover(self, client: Kernel) -> None: - response = client.agents.auth.invocations.with_raw_response.discover( - invocation_id="invocation_id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - invocation = response.parse() - assert_matches_type(AgentAuthDiscoverResponse, invocation, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_discover(self, client: Kernel) -> None: - with client.agents.auth.invocations.with_streaming_response.discover( - invocation_id="invocation_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - invocation = response.parse() - assert_matches_type(AgentAuthDiscoverResponse, invocation, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_discover(self, client: Kernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): - client.agents.auth.invocations.with_raw_response.discover( - invocation_id="", - ) - @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_exchange(self, client: Kernel) -> None: @@ -209,7 +153,7 @@ def test_path_params_exchange(self, client: Kernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_method_submit(self, client: Kernel) -> None: + def test_method_submit_overload_1(self, client: Kernel) -> None: invocation = client.agents.auth.invocations.submit( invocation_id="invocation_id", field_values={ @@ -221,7 +165,7 @@ def test_method_submit(self, client: Kernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_raw_response_submit(self, client: Kernel) -> None: + def test_raw_response_submit_overload_1(self, client: Kernel) -> None: response = client.agents.auth.invocations.with_raw_response.submit( invocation_id="invocation_id", field_values={ @@ -237,7 +181,7 @@ def test_raw_response_submit(self, client: Kernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_streaming_response_submit(self, client: Kernel) -> None: + def test_streaming_response_submit_overload_1(self, client: Kernel) -> None: with client.agents.auth.invocations.with_streaming_response.submit( invocation_id="invocation_id", field_values={ @@ -255,7 +199,7 @@ def test_streaming_response_submit(self, client: Kernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - def test_path_params_submit(self, client: Kernel) -> None: + def test_path_params_submit_overload_1(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): client.agents.auth.invocations.with_raw_response.submit( invocation_id="", @@ -265,6 +209,52 @@ def test_path_params_submit(self, client: Kernel) -> None: }, ) + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_submit_overload_2(self, client: Kernel) -> None: + invocation = client.agents.auth.invocations.submit( + invocation_id="invocation_id", + sso_button="xpath=//button[contains(text(), 'Continue with Google')]", + ) + assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_submit_overload_2(self, client: Kernel) -> None: + response = client.agents.auth.invocations.with_raw_response.submit( + invocation_id="invocation_id", + sso_button="xpath=//button[contains(text(), 'Continue with Google')]", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + invocation = response.parse() + assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_submit_overload_2(self, client: Kernel) -> None: + with client.agents.auth.invocations.with_streaming_response.submit( + invocation_id="invocation_id", + sso_button="xpath=//button[contains(text(), 'Continue with Google')]", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + invocation = response.parse() + assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_submit_overload_2(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): + client.agents.auth.invocations.with_raw_response.submit( + invocation_id="", + sso_button="xpath=//button[contains(text(), 'Continue with Google')]", + ) + class TestAsyncInvocations: parametrize = pytest.mark.parametrize( @@ -356,57 +346,6 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_discover(self, async_client: AsyncKernel) -> None: - invocation = await async_client.agents.auth.invocations.discover( - invocation_id="invocation_id", - ) - assert_matches_type(AgentAuthDiscoverResponse, invocation, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_discover_with_all_params(self, async_client: AsyncKernel) -> None: - invocation = await async_client.agents.auth.invocations.discover( - invocation_id="invocation_id", - login_url="https://doordash.com/account/login", - ) - assert_matches_type(AgentAuthDiscoverResponse, invocation, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_discover(self, async_client: AsyncKernel) -> None: - response = await async_client.agents.auth.invocations.with_raw_response.discover( - invocation_id="invocation_id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - invocation = await response.parse() - assert_matches_type(AgentAuthDiscoverResponse, invocation, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_discover(self, async_client: AsyncKernel) -> None: - async with async_client.agents.auth.invocations.with_streaming_response.discover( - invocation_id="invocation_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - invocation = await response.parse() - assert_matches_type(AgentAuthDiscoverResponse, invocation, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_discover(self, async_client: AsyncKernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): - await async_client.agents.auth.invocations.with_raw_response.discover( - invocation_id="", - ) - @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_exchange(self, async_client: AsyncKernel) -> None: @@ -455,7 +394,7 @@ async def test_path_params_exchange(self, async_client: AsyncKernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_method_submit(self, async_client: AsyncKernel) -> None: + async def test_method_submit_overload_1(self, async_client: AsyncKernel) -> None: invocation = await async_client.agents.auth.invocations.submit( invocation_id="invocation_id", field_values={ @@ -467,7 +406,7 @@ async def test_method_submit(self, async_client: AsyncKernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_raw_response_submit(self, async_client: AsyncKernel) -> None: + async def test_raw_response_submit_overload_1(self, async_client: AsyncKernel) -> None: response = await async_client.agents.auth.invocations.with_raw_response.submit( invocation_id="invocation_id", field_values={ @@ -483,7 +422,7 @@ async def test_raw_response_submit(self, async_client: AsyncKernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_streaming_response_submit(self, async_client: AsyncKernel) -> None: + async def test_streaming_response_submit_overload_1(self, async_client: AsyncKernel) -> None: async with async_client.agents.auth.invocations.with_streaming_response.submit( invocation_id="invocation_id", field_values={ @@ -501,7 +440,7 @@ async def test_streaming_response_submit(self, async_client: AsyncKernel) -> Non @pytest.mark.skip(reason="Prism tests are disabled") @parametrize - async def test_path_params_submit(self, async_client: AsyncKernel) -> None: + async def test_path_params_submit_overload_1(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): await async_client.agents.auth.invocations.with_raw_response.submit( invocation_id="", @@ -510,3 +449,49 @@ async def test_path_params_submit(self, async_client: AsyncKernel) -> None: "password": "********", }, ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_submit_overload_2(self, async_client: AsyncKernel) -> None: + invocation = await async_client.agents.auth.invocations.submit( + invocation_id="invocation_id", + sso_button="xpath=//button[contains(text(), 'Continue with Google')]", + ) + assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_submit_overload_2(self, async_client: AsyncKernel) -> None: + response = await async_client.agents.auth.invocations.with_raw_response.submit( + invocation_id="invocation_id", + sso_button="xpath=//button[contains(text(), 'Continue with Google')]", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + invocation = await response.parse() + assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_submit_overload_2(self, async_client: AsyncKernel) -> None: + async with async_client.agents.auth.invocations.with_streaming_response.submit( + invocation_id="invocation_id", + sso_button="xpath=//button[contains(text(), 'Continue with Google')]", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + invocation = await response.parse() + assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_submit_overload_2(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): + await async_client.agents.auth.invocations.with_raw_response.submit( + invocation_id="", + sso_button="xpath=//button[contains(text(), 'Continue with Google')]", + ) diff --git a/tests/api_resources/agents/test_auth.py b/tests/api_resources/agents/test_auth.py index 192361ad..9855ef85 100644 --- a/tests/api_resources/agents/test_auth.py +++ b/tests/api_resources/agents/test_auth.py @@ -10,7 +10,7 @@ from kernel import Kernel, AsyncKernel from tests.utils import assert_matches_type from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination -from kernel.types.agents import AuthAgent, ReauthResponse +from kernel.types.agents import AuthAgent base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -22,8 +22,8 @@ class TestAuth: @parametrize def test_method_create(self, client: Kernel) -> None: auth = client.agents.auth.create( + domain="netflix.com", profile_name="user-123", - target_domain="netflix.com", ) assert_matches_type(AuthAgent, auth, path=["response"]) @@ -31,8 +31,9 @@ def test_method_create(self, client: Kernel) -> None: @parametrize def test_method_create_with_all_params(self, client: Kernel) -> None: auth = client.agents.auth.create( + domain="netflix.com", profile_name="user-123", - target_domain="netflix.com", + allowed_domains=["login.netflix.com", "auth.netflix.com"], credential_name="my-netflix-login", login_url="https://netflix.com/login", proxy={"proxy_id": "proxy_id"}, @@ -43,8 +44,8 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: @parametrize def test_raw_response_create(self, client: Kernel) -> None: response = client.agents.auth.with_raw_response.create( + domain="netflix.com", profile_name="user-123", - target_domain="netflix.com", ) assert response.is_closed is True @@ -56,8 +57,8 @@ def test_raw_response_create(self, client: Kernel) -> None: @parametrize def test_streaming_response_create(self, client: Kernel) -> None: with client.agents.auth.with_streaming_response.create( + domain="netflix.com", profile_name="user-123", - target_domain="netflix.com", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -119,10 +120,10 @@ def test_method_list(self, client: Kernel) -> None: @parametrize def test_method_list_with_all_params(self, client: Kernel) -> None: auth = client.agents.auth.list( + domain="domain", limit=100, offset=0, profile_name="profile_name", - target_domain="target_domain", ) assert_matches_type(SyncOffsetPagination[AuthAgent], auth, path=["response"]) @@ -190,48 +191,6 @@ def test_path_params_delete(self, client: Kernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_reauth(self, client: Kernel) -> None: - auth = client.agents.auth.reauth( - "id", - ) - assert_matches_type(ReauthResponse, auth, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_reauth(self, client: Kernel) -> None: - response = client.agents.auth.with_raw_response.reauth( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - auth = response.parse() - assert_matches_type(ReauthResponse, auth, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_reauth(self, client: Kernel) -> None: - with client.agents.auth.with_streaming_response.reauth( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - auth = response.parse() - assert_matches_type(ReauthResponse, auth, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_reauth(self, client: Kernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - client.agents.auth.with_raw_response.reauth( - "", - ) - class TestAsyncAuth: parametrize = pytest.mark.parametrize( @@ -242,8 +201,8 @@ class TestAsyncAuth: @parametrize async def test_method_create(self, async_client: AsyncKernel) -> None: auth = await async_client.agents.auth.create( + domain="netflix.com", profile_name="user-123", - target_domain="netflix.com", ) assert_matches_type(AuthAgent, auth, path=["response"]) @@ -251,8 +210,9 @@ async def test_method_create(self, async_client: AsyncKernel) -> None: @parametrize async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: auth = await async_client.agents.auth.create( + domain="netflix.com", profile_name="user-123", - target_domain="netflix.com", + allowed_domains=["login.netflix.com", "auth.netflix.com"], credential_name="my-netflix-login", login_url="https://netflix.com/login", proxy={"proxy_id": "proxy_id"}, @@ -263,8 +223,8 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> @parametrize async def test_raw_response_create(self, async_client: AsyncKernel) -> None: response = await async_client.agents.auth.with_raw_response.create( + domain="netflix.com", profile_name="user-123", - target_domain="netflix.com", ) assert response.is_closed is True @@ -276,8 +236,8 @@ async def test_raw_response_create(self, async_client: AsyncKernel) -> None: @parametrize async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: async with async_client.agents.auth.with_streaming_response.create( + domain="netflix.com", profile_name="user-123", - target_domain="netflix.com", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -339,10 +299,10 @@ async def test_method_list(self, async_client: AsyncKernel) -> None: @parametrize async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: auth = await async_client.agents.auth.list( + domain="domain", limit=100, offset=0, profile_name="profile_name", - target_domain="target_domain", ) assert_matches_type(AsyncOffsetPagination[AuthAgent], auth, path=["response"]) @@ -409,45 +369,3 @@ async def test_path_params_delete(self, async_client: AsyncKernel) -> None: await async_client.agents.auth.with_raw_response.delete( "", ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_reauth(self, async_client: AsyncKernel) -> None: - auth = await async_client.agents.auth.reauth( - "id", - ) - assert_matches_type(ReauthResponse, auth, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_reauth(self, async_client: AsyncKernel) -> None: - response = await async_client.agents.auth.with_raw_response.reauth( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - auth = await response.parse() - assert_matches_type(ReauthResponse, auth, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_reauth(self, async_client: AsyncKernel) -> None: - async with async_client.agents.auth.with_streaming_response.reauth( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - auth = await response.parse() - assert_matches_type(ReauthResponse, auth, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_reauth(self, async_client: AsyncKernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - await async_client.agents.auth.with_raw_response.reauth( - "", - ) diff --git a/tests/api_resources/test_credentials.py b/tests/api_resources/test_credentials.py index 00c7635e..b6098685 100644 --- a/tests/api_resources/test_credentials.py +++ b/tests/api_resources/test_credentials.py @@ -9,7 +9,10 @@ from kernel import Kernel, AsyncKernel from tests.utils import assert_matches_type -from kernel.types import Credential +from kernel.types import ( + Credential, + CredentialTotpCodeResponse, +) from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -31,6 +34,21 @@ def test_method_create(self, client: Kernel) -> None: ) assert_matches_type(Credential, credential, path=["response"]) + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_create_with_all_params(self, client: Kernel) -> None: + credential = client.credentials.create( + domain="netflix.com", + name="my-netflix-login", + values={ + "username": "user@example.com", + "password": "mysecretpassword", + }, + sso_provider="google", + totp_secret="JBSWY3DPEHPK3PXP", + ) + assert_matches_type(Credential, credential, path=["response"]) + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_create(self, client: Kernel) -> None: @@ -71,7 +89,7 @@ def test_streaming_response_create(self, client: Kernel) -> None: @parametrize def test_method_retrieve(self, client: Kernel) -> None: credential = client.credentials.retrieve( - "id", + "id_or_name", ) assert_matches_type(Credential, credential, path=["response"]) @@ -79,7 +97,7 @@ def test_method_retrieve(self, client: Kernel) -> None: @parametrize def test_raw_response_retrieve(self, client: Kernel) -> None: response = client.credentials.with_raw_response.retrieve( - "id", + "id_or_name", ) assert response.is_closed is True @@ -91,7 +109,7 @@ def test_raw_response_retrieve(self, client: Kernel) -> None: @parametrize def test_streaming_response_retrieve(self, client: Kernel) -> None: with client.credentials.with_streaming_response.retrieve( - "id", + "id_or_name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -104,7 +122,7 @@ def test_streaming_response_retrieve(self, client: Kernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_path_params_retrieve(self, client: Kernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): client.credentials.with_raw_response.retrieve( "", ) @@ -113,7 +131,7 @@ def test_path_params_retrieve(self, client: Kernel) -> None: @parametrize def test_method_update(self, client: Kernel) -> None: credential = client.credentials.update( - id="id", + id_or_name="id_or_name", ) assert_matches_type(Credential, credential, path=["response"]) @@ -121,8 +139,10 @@ def test_method_update(self, client: Kernel) -> None: @parametrize def test_method_update_with_all_params(self, client: Kernel) -> None: credential = client.credentials.update( - id="id", + id_or_name="id_or_name", name="my-updated-login", + sso_provider="google", + totp_secret="JBSWY3DPEHPK3PXP", values={ "username": "user@example.com", "password": "newpassword", @@ -134,7 +154,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None: @parametrize def test_raw_response_update(self, client: Kernel) -> None: response = client.credentials.with_raw_response.update( - id="id", + id_or_name="id_or_name", ) assert response.is_closed is True @@ -146,7 +166,7 @@ def test_raw_response_update(self, client: Kernel) -> None: @parametrize def test_streaming_response_update(self, client: Kernel) -> None: with client.credentials.with_streaming_response.update( - id="id", + id_or_name="id_or_name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -159,9 +179,9 @@ def test_streaming_response_update(self, client: Kernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_path_params_update(self, client: Kernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): client.credentials.with_raw_response.update( - id="", + id_or_name="", ) @pytest.mark.skip(reason="Prism tests are disabled") @@ -206,7 +226,7 @@ def test_streaming_response_list(self, client: Kernel) -> None: @parametrize def test_method_delete(self, client: Kernel) -> None: credential = client.credentials.delete( - "id", + "id_or_name", ) assert credential is None @@ -214,7 +234,7 @@ def test_method_delete(self, client: Kernel) -> None: @parametrize def test_raw_response_delete(self, client: Kernel) -> None: response = client.credentials.with_raw_response.delete( - "id", + "id_or_name", ) assert response.is_closed is True @@ -226,7 +246,7 @@ def test_raw_response_delete(self, client: Kernel) -> None: @parametrize def test_streaming_response_delete(self, client: Kernel) -> None: with client.credentials.with_streaming_response.delete( - "id", + "id_or_name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -239,11 +259,53 @@ def test_streaming_response_delete(self, client: Kernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_path_params_delete(self, client: Kernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): client.credentials.with_raw_response.delete( "", ) + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_totp_code(self, client: Kernel) -> None: + credential = client.credentials.totp_code( + "id_or_name", + ) + assert_matches_type(CredentialTotpCodeResponse, credential, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_totp_code(self, client: Kernel) -> None: + response = client.credentials.with_raw_response.totp_code( + "id_or_name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + credential = response.parse() + assert_matches_type(CredentialTotpCodeResponse, credential, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_totp_code(self, client: Kernel) -> None: + with client.credentials.with_streaming_response.totp_code( + "id_or_name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + credential = response.parse() + assert_matches_type(CredentialTotpCodeResponse, credential, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_totp_code(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): + client.credentials.with_raw_response.totp_code( + "", + ) + class TestAsyncCredentials: parametrize = pytest.mark.parametrize( @@ -263,6 +325,21 @@ async def test_method_create(self, async_client: AsyncKernel) -> None: ) assert_matches_type(Credential, credential, path=["response"]) + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: + credential = await async_client.credentials.create( + domain="netflix.com", + name="my-netflix-login", + values={ + "username": "user@example.com", + "password": "mysecretpassword", + }, + sso_provider="google", + totp_secret="JBSWY3DPEHPK3PXP", + ) + assert_matches_type(Credential, credential, path=["response"]) + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_create(self, async_client: AsyncKernel) -> None: @@ -303,7 +380,7 @@ async def test_streaming_response_create(self, async_client: AsyncKernel) -> Non @parametrize async def test_method_retrieve(self, async_client: AsyncKernel) -> None: credential = await async_client.credentials.retrieve( - "id", + "id_or_name", ) assert_matches_type(Credential, credential, path=["response"]) @@ -311,7 +388,7 @@ async def test_method_retrieve(self, async_client: AsyncKernel) -> None: @parametrize async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: response = await async_client.credentials.with_raw_response.retrieve( - "id", + "id_or_name", ) assert response.is_closed is True @@ -323,7 +400,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: async with async_client.credentials.with_streaming_response.retrieve( - "id", + "id_or_name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -336,7 +413,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> N @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): await async_client.credentials.with_raw_response.retrieve( "", ) @@ -345,7 +422,7 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: @parametrize async def test_method_update(self, async_client: AsyncKernel) -> None: credential = await async_client.credentials.update( - id="id", + id_or_name="id_or_name", ) assert_matches_type(Credential, credential, path=["response"]) @@ -353,8 +430,10 @@ async def test_method_update(self, async_client: AsyncKernel) -> None: @parametrize async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> None: credential = await async_client.credentials.update( - id="id", + id_or_name="id_or_name", name="my-updated-login", + sso_provider="google", + totp_secret="JBSWY3DPEHPK3PXP", values={ "username": "user@example.com", "password": "newpassword", @@ -366,7 +445,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> @parametrize async def test_raw_response_update(self, async_client: AsyncKernel) -> None: response = await async_client.credentials.with_raw_response.update( - id="id", + id_or_name="id_or_name", ) assert response.is_closed is True @@ -378,7 +457,7 @@ async def test_raw_response_update(self, async_client: AsyncKernel) -> None: @parametrize async def test_streaming_response_update(self, async_client: AsyncKernel) -> None: async with async_client.credentials.with_streaming_response.update( - id="id", + id_or_name="id_or_name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -391,9 +470,9 @@ async def test_streaming_response_update(self, async_client: AsyncKernel) -> Non @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_path_params_update(self, async_client: AsyncKernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): await async_client.credentials.with_raw_response.update( - id="", + id_or_name="", ) @pytest.mark.skip(reason="Prism tests are disabled") @@ -438,7 +517,7 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: @parametrize async def test_method_delete(self, async_client: AsyncKernel) -> None: credential = await async_client.credentials.delete( - "id", + "id_or_name", ) assert credential is None @@ -446,7 +525,7 @@ async def test_method_delete(self, async_client: AsyncKernel) -> None: @parametrize async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: response = await async_client.credentials.with_raw_response.delete( - "id", + "id_or_name", ) assert response.is_closed is True @@ -458,7 +537,7 @@ async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: @parametrize async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None: async with async_client.credentials.with_streaming_response.delete( - "id", + "id_or_name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -471,7 +550,49 @@ async def test_streaming_response_delete(self, async_client: AsyncKernel) -> Non @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_path_params_delete(self, async_client: AsyncKernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): await async_client.credentials.with_raw_response.delete( "", ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_totp_code(self, async_client: AsyncKernel) -> None: + credential = await async_client.credentials.totp_code( + "id_or_name", + ) + assert_matches_type(CredentialTotpCodeResponse, credential, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_totp_code(self, async_client: AsyncKernel) -> None: + response = await async_client.credentials.with_raw_response.totp_code( + "id_or_name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + credential = await response.parse() + assert_matches_type(CredentialTotpCodeResponse, credential, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_totp_code(self, async_client: AsyncKernel) -> None: + async with async_client.credentials.with_streaming_response.totp_code( + "id_or_name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + credential = await response.parse() + assert_matches_type(CredentialTotpCodeResponse, credential, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_totp_code(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): + await async_client.credentials.with_raw_response.totp_code( + "", + ) From eea7e5635e786a5cdcbd12803cc9f57c48668d7b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 7 Jan 2026 18:05:46 +0000 Subject: [PATCH 251/448] feat(api): update production repos --- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/release-doctor.yml | 2 +- .stats.yml | 2 +- CONTRIBUTING.md | 4 ++-- README.md | 6 +++--- pyproject.toml | 6 +++--- src/kernel/_files.py | 2 +- src/kernel/resources/agents/agents.py | 8 ++++---- src/kernel/resources/agents/auth/auth.py | 8 ++++---- src/kernel/resources/agents/auth/invocations.py | 8 ++++---- src/kernel/resources/apps.py | 8 ++++---- src/kernel/resources/browser_pools.py | 8 ++++---- src/kernel/resources/browsers/browsers.py | 8 ++++---- src/kernel/resources/browsers/computer.py | 8 ++++---- src/kernel/resources/browsers/fs/fs.py | 8 ++++---- src/kernel/resources/browsers/fs/watch.py | 8 ++++---- src/kernel/resources/browsers/logs.py | 8 ++++---- src/kernel/resources/browsers/playwright.py | 8 ++++---- src/kernel/resources/browsers/process.py | 8 ++++---- src/kernel/resources/browsers/replays.py | 8 ++++---- src/kernel/resources/credentials.py | 8 ++++---- src/kernel/resources/deployments.py | 8 ++++---- src/kernel/resources/extensions.py | 8 ++++---- src/kernel/resources/invocations.py | 8 ++++---- src/kernel/resources/profiles.py | 8 ++++---- src/kernel/resources/proxies.py | 8 ++++---- 26 files changed, 88 insertions(+), 88 deletions(-) diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 120241d9..994e6250 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -1,6 +1,6 @@ # 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/onkernel/kernel-python-sdk/actions/workflows/publish-pypi.yml +# You can run this workflow by navigating to https://www.github.com/kernel/kernel-python-sdk/actions/workflows/publish-pypi.yml name: Publish PyPI on: workflow_dispatch: diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index 5e7787d0..ba1be2c7 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -9,7 +9,7 @@ jobs: release_doctor: name: release doctor runs-on: ubuntu-latest - if: github.repository == 'onkernel/kernel-python-sdk' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') + if: github.repository == 'kernel/kernel-python-sdk' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') steps: - uses: actions/checkout@v4 diff --git a/.stats.yml b/.stats.yml index 434275ee..9ab4346b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 89 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-8d66dbedea5b240936b338809f272568ca84a452fc13dbda835479f2ec068b41.yml openapi_spec_hash: 7c499bfce2e996f1fff5e7791cea390e -config_hash: fcc2db3ed48ab4e8d1b588d31d394a23 +config_hash: 2ee8c7057fa9b05cd0dabd23247c40ec diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f05c930b..9cb624fb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,7 +62,7 @@ If you’d like to use the repository from source, you can either install from g To install via git: ```sh -$ pip install git+ssh://git@github.com/onkernel/kernel-python-sdk.git +$ pip install git+ssh://git@github.com/kernel/kernel-python-sdk.git ``` Alternatively, you can build from source and install the wheel file: @@ -120,7 +120,7 @@ the changes aren't made through the automated pipeline, you may want to make rel ### Publish with a GitHub workflow -You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/onkernel/kernel-python-sdk/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. +You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/kernel/kernel-python-sdk/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. ### Publish manually diff --git a/README.md b/README.md index 6d1dd19b..d3e4341e 100644 --- a/README.md +++ b/README.md @@ -363,9 +363,9 @@ browser = response.parse() # get the object that `browsers.create()` would have print(browser.session_id) ``` -These methods return an [`APIResponse`](https://github.com/onkernel/kernel-python-sdk/tree/main/src/kernel/_response.py) object. +These methods return an [`APIResponse`](https://github.com/kernel/kernel-python-sdk/tree/main/src/kernel/_response.py) object. -The async client returns an [`AsyncAPIResponse`](https://github.com/onkernel/kernel-python-sdk/tree/main/src/kernel/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. +The async client returns an [`AsyncAPIResponse`](https://github.com/kernel/kernel-python-sdk/tree/main/src/kernel/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. #### `.with_streaming_response` @@ -471,7 +471,7 @@ This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) con We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. -We are keen for your feedback; please open an [issue](https://www.github.com/onkernel/kernel-python-sdk/issues) with questions, bugs, or suggestions. +We are keen for your feedback; please open an [issue](https://www.github.com/kernel/kernel-python-sdk/issues) with questions, bugs, or suggestions. ### Determining the installed version diff --git a/pyproject.toml b/pyproject.toml index 770392c1..22aadee5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,8 +37,8 @@ classifiers = [ ] [project.urls] -Homepage = "https://github.com/onkernel/kernel-python-sdk" -Repository = "https://github.com/onkernel/kernel-python-sdk" +Homepage = "https://github.com/kernel/kernel-python-sdk" +Repository = "https://github.com/kernel/kernel-python-sdk" [project.optional-dependencies] aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.9"] @@ -126,7 +126,7 @@ path = "README.md" [[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]] # replace relative links with absolute links pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)' -replacement = '[\1](https://github.com/onkernel/kernel-python-sdk/tree/main/\g<2>)' +replacement = '[\1](https://github.com/kernel/kernel-python-sdk/tree/main/\g<2>)' [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/src/kernel/_files.py b/src/kernel/_files.py index 9a6dd194..bbef8bfb 100644 --- a/src/kernel/_files.py +++ b/src/kernel/_files.py @@ -34,7 +34,7 @@ def assert_is_file_content(obj: object, *, key: str | None = None) -> None: if not is_file_content(obj): prefix = f"Expected entry at `{key}`" if key is not None else f"Expected file input `{obj!r}`" raise RuntimeError( - f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead. See https://github.com/onkernel/kernel-python-sdk/tree/main#file-uploads" + f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead. See https://github.com/kernel/kernel-python-sdk/tree/main#file-uploads" ) from None diff --git a/src/kernel/resources/agents/agents.py b/src/kernel/resources/agents/agents.py index b7bb580c..6999bd58 100644 --- a/src/kernel/resources/agents/agents.py +++ b/src/kernel/resources/agents/agents.py @@ -27,7 +27,7 @@ def with_raw_response(self) -> AgentsResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return AgentsResourceWithRawResponse(self) @@ -36,7 +36,7 @@ def with_streaming_response(self) -> AgentsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return AgentsResourceWithStreamingResponse(self) @@ -52,7 +52,7 @@ def with_raw_response(self) -> AsyncAgentsResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return AsyncAgentsResourceWithRawResponse(self) @@ -61,7 +61,7 @@ def with_streaming_response(self) -> AsyncAgentsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return AsyncAgentsResourceWithStreamingResponse(self) diff --git a/src/kernel/resources/agents/auth/auth.py b/src/kernel/resources/agents/auth/auth.py index f4a02767..4a541f73 100644 --- a/src/kernel/resources/agents/auth/auth.py +++ b/src/kernel/resources/agents/auth/auth.py @@ -41,7 +41,7 @@ def with_raw_response(self) -> AuthResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return AuthResourceWithRawResponse(self) @@ -50,7 +50,7 @@ def with_streaming_response(self) -> AuthResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return AuthResourceWithStreamingResponse(self) @@ -262,7 +262,7 @@ def with_raw_response(self) -> AsyncAuthResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return AsyncAuthResourceWithRawResponse(self) @@ -271,7 +271,7 @@ def with_streaming_response(self) -> AsyncAuthResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return AsyncAuthResourceWithStreamingResponse(self) diff --git a/src/kernel/resources/agents/auth/invocations.py b/src/kernel/resources/agents/auth/invocations.py index 34ab614d..aa1c4da7 100644 --- a/src/kernel/resources/agents/auth/invocations.py +++ b/src/kernel/resources/agents/auth/invocations.py @@ -34,7 +34,7 @@ def with_raw_response(self) -> InvocationsResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return InvocationsResourceWithRawResponse(self) @@ -43,7 +43,7 @@ def with_streaming_response(self) -> InvocationsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return InvocationsResourceWithStreamingResponse(self) @@ -272,7 +272,7 @@ def with_raw_response(self) -> AsyncInvocationsResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return AsyncInvocationsResourceWithRawResponse(self) @@ -281,7 +281,7 @@ def with_streaming_response(self) -> AsyncInvocationsResourceWithStreamingRespon """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return AsyncInvocationsResourceWithStreamingResponse(self) diff --git a/src/kernel/resources/apps.py b/src/kernel/resources/apps.py index b803299d..0443e73a 100644 --- a/src/kernel/resources/apps.py +++ b/src/kernel/resources/apps.py @@ -29,7 +29,7 @@ def with_raw_response(self) -> AppsResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return AppsResourceWithRawResponse(self) @@ -38,7 +38,7 @@ def with_streaming_response(self) -> AppsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return AppsResourceWithStreamingResponse(self) @@ -106,7 +106,7 @@ def with_raw_response(self) -> AsyncAppsResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return AsyncAppsResourceWithRawResponse(self) @@ -115,7 +115,7 @@ def with_streaming_response(self) -> AsyncAppsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return AsyncAppsResourceWithStreamingResponse(self) diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py index 8c480ed5..5a4bf61b 100644 --- a/src/kernel/resources/browser_pools.py +++ b/src/kernel/resources/browser_pools.py @@ -41,7 +41,7 @@ def with_raw_response(self) -> BrowserPoolsResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return BrowserPoolsResourceWithRawResponse(self) @@ -50,7 +50,7 @@ def with_streaming_response(self) -> BrowserPoolsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return BrowserPoolsResourceWithStreamingResponse(self) @@ -475,7 +475,7 @@ def with_raw_response(self) -> AsyncBrowserPoolsResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return AsyncBrowserPoolsResourceWithRawResponse(self) @@ -484,7 +484,7 @@ def with_streaming_response(self) -> AsyncBrowserPoolsResourceWithStreamingRespo """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return AsyncBrowserPoolsResourceWithStreamingResponse(self) diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index cbd17736..8050a7dc 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -115,7 +115,7 @@ def with_raw_response(self) -> BrowsersResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return BrowsersResourceWithRawResponse(self) @@ -124,7 +124,7 @@ def with_streaming_response(self) -> BrowsersResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return BrowsersResourceWithStreamingResponse(self) @@ -460,7 +460,7 @@ def with_raw_response(self) -> AsyncBrowsersResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return AsyncBrowsersResourceWithRawResponse(self) @@ -469,7 +469,7 @@ def with_streaming_response(self) -> AsyncBrowsersResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return AsyncBrowsersResourceWithStreamingResponse(self) diff --git a/src/kernel/resources/browsers/computer.py b/src/kernel/resources/browsers/computer.py index 87d377fd..c23dd3db 100644 --- a/src/kernel/resources/browsers/computer.py +++ b/src/kernel/resources/browsers/computer.py @@ -48,7 +48,7 @@ def with_raw_response(self) -> ComputerResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return ComputerResourceWithRawResponse(self) @@ -57,7 +57,7 @@ def with_streaming_response(self) -> ComputerResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return ComputerResourceWithStreamingResponse(self) @@ -486,7 +486,7 @@ def with_raw_response(self) -> AsyncComputerResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return AsyncComputerResourceWithRawResponse(self) @@ -495,7 +495,7 @@ def with_streaming_response(self) -> AsyncComputerResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return AsyncComputerResourceWithStreamingResponse(self) diff --git a/src/kernel/resources/browsers/fs/fs.py b/src/kernel/resources/browsers/fs/fs.py index ff0cc48a..0da0bddd 100644 --- a/src/kernel/resources/browsers/fs/fs.py +++ b/src/kernel/resources/browsers/fs/fs.py @@ -65,7 +65,7 @@ def with_raw_response(self) -> FsResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return FsResourceWithRawResponse(self) @@ -74,7 +74,7 @@ def with_streaming_response(self) -> FsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return FsResourceWithStreamingResponse(self) @@ -624,7 +624,7 @@ def with_raw_response(self) -> AsyncFsResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return AsyncFsResourceWithRawResponse(self) @@ -633,7 +633,7 @@ def with_streaming_response(self) -> AsyncFsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return AsyncFsResourceWithStreamingResponse(self) diff --git a/src/kernel/resources/browsers/fs/watch.py b/src/kernel/resources/browsers/fs/watch.py index ad26f2ae..2a5c1e30 100644 --- a/src/kernel/resources/browsers/fs/watch.py +++ b/src/kernel/resources/browsers/fs/watch.py @@ -30,7 +30,7 @@ def with_raw_response(self) -> WatchResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return WatchResourceWithRawResponse(self) @@ -39,7 +39,7 @@ def with_streaming_response(self) -> WatchResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return WatchResourceWithStreamingResponse(self) @@ -173,7 +173,7 @@ def with_raw_response(self) -> AsyncWatchResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return AsyncWatchResourceWithRawResponse(self) @@ -182,7 +182,7 @@ def with_streaming_response(self) -> AsyncWatchResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return AsyncWatchResourceWithStreamingResponse(self) diff --git a/src/kernel/resources/browsers/logs.py b/src/kernel/resources/browsers/logs.py index 1fd291d4..ab97a70d 100644 --- a/src/kernel/resources/browsers/logs.py +++ b/src/kernel/resources/browsers/logs.py @@ -31,7 +31,7 @@ def with_raw_response(self) -> LogsResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return LogsResourceWithRawResponse(self) @@ -40,7 +40,7 @@ def with_streaming_response(self) -> LogsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return LogsResourceWithStreamingResponse(self) @@ -108,7 +108,7 @@ def with_raw_response(self) -> AsyncLogsResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return AsyncLogsResourceWithRawResponse(self) @@ -117,7 +117,7 @@ def with_streaming_response(self) -> AsyncLogsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return AsyncLogsResourceWithStreamingResponse(self) diff --git a/src/kernel/resources/browsers/playwright.py b/src/kernel/resources/browsers/playwright.py index c168a4a7..5c47e3bf 100644 --- a/src/kernel/resources/browsers/playwright.py +++ b/src/kernel/resources/browsers/playwright.py @@ -28,7 +28,7 @@ def with_raw_response(self) -> PlaywrightResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return PlaywrightResourceWithRawResponse(self) @@ -37,7 +37,7 @@ def with_streaming_response(self) -> PlaywrightResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return PlaywrightResourceWithStreamingResponse(self) @@ -102,7 +102,7 @@ def with_raw_response(self) -> AsyncPlaywrightResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return AsyncPlaywrightResourceWithRawResponse(self) @@ -111,7 +111,7 @@ def with_streaming_response(self) -> AsyncPlaywrightResourceWithStreamingRespons """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return AsyncPlaywrightResourceWithStreamingResponse(self) diff --git a/src/kernel/resources/browsers/process.py b/src/kernel/resources/browsers/process.py index 2bdaeebe..f5c4341e 100644 --- a/src/kernel/resources/browsers/process.py +++ b/src/kernel/resources/browsers/process.py @@ -37,7 +37,7 @@ def with_raw_response(self) -> ProcessResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return ProcessResourceWithRawResponse(self) @@ -46,7 +46,7 @@ def with_streaming_response(self) -> ProcessResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return ProcessResourceWithStreamingResponse(self) @@ -345,7 +345,7 @@ def with_raw_response(self) -> AsyncProcessResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return AsyncProcessResourceWithRawResponse(self) @@ -354,7 +354,7 @@ def with_streaming_response(self) -> AsyncProcessResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return AsyncProcessResourceWithStreamingResponse(self) diff --git a/src/kernel/resources/browsers/replays.py b/src/kernel/resources/browsers/replays.py index 9f15554a..8a1d1996 100644 --- a/src/kernel/resources/browsers/replays.py +++ b/src/kernel/resources/browsers/replays.py @@ -37,7 +37,7 @@ def with_raw_response(self) -> ReplaysResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return ReplaysResourceWithRawResponse(self) @@ -46,7 +46,7 @@ def with_streaming_response(self) -> ReplaysResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return ReplaysResourceWithStreamingResponse(self) @@ -211,7 +211,7 @@ def with_raw_response(self) -> AsyncReplaysResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return AsyncReplaysResourceWithRawResponse(self) @@ -220,7 +220,7 @@ def with_streaming_response(self) -> AsyncReplaysResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return AsyncReplaysResourceWithStreamingResponse(self) diff --git a/src/kernel/resources/credentials.py b/src/kernel/resources/credentials.py index 85e0c8a0..30e72e84 100644 --- a/src/kernel/resources/credentials.py +++ b/src/kernel/resources/credentials.py @@ -32,7 +32,7 @@ def with_raw_response(self) -> CredentialsResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return CredentialsResourceWithRawResponse(self) @@ -41,7 +41,7 @@ def with_streaming_response(self) -> CredentialsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return CredentialsResourceWithStreamingResponse(self) @@ -327,7 +327,7 @@ def with_raw_response(self) -> AsyncCredentialsResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return AsyncCredentialsResourceWithRawResponse(self) @@ -336,7 +336,7 @@ def with_streaming_response(self) -> AsyncCredentialsResourceWithStreamingRespon """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return AsyncCredentialsResourceWithStreamingResponse(self) diff --git a/src/kernel/resources/deployments.py b/src/kernel/resources/deployments.py index bdc200f1..f924531c 100644 --- a/src/kernel/resources/deployments.py +++ b/src/kernel/resources/deployments.py @@ -36,7 +36,7 @@ def with_raw_response(self) -> DeploymentsResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return DeploymentsResourceWithRawResponse(self) @@ -45,7 +45,7 @@ def with_streaming_response(self) -> DeploymentsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return DeploymentsResourceWithStreamingResponse(self) @@ -259,7 +259,7 @@ def with_raw_response(self) -> AsyncDeploymentsResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return AsyncDeploymentsResourceWithRawResponse(self) @@ -268,7 +268,7 @@ def with_streaming_response(self) -> AsyncDeploymentsResourceWithStreamingRespon """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return AsyncDeploymentsResourceWithStreamingResponse(self) diff --git a/src/kernel/resources/extensions.py b/src/kernel/resources/extensions.py index 2f868716..69497b1f 100644 --- a/src/kernel/resources/extensions.py +++ b/src/kernel/resources/extensions.py @@ -40,7 +40,7 @@ def with_raw_response(self) -> ExtensionsResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return ExtensionsResourceWithRawResponse(self) @@ -49,7 +49,7 @@ def with_streaming_response(self) -> ExtensionsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return ExtensionsResourceWithStreamingResponse(self) @@ -247,7 +247,7 @@ def with_raw_response(self) -> AsyncExtensionsResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return AsyncExtensionsResourceWithRawResponse(self) @@ -256,7 +256,7 @@ def with_streaming_response(self) -> AsyncExtensionsResourceWithStreamingRespons """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return AsyncExtensionsResourceWithStreamingResponse(self) diff --git a/src/kernel/resources/invocations.py b/src/kernel/resources/invocations.py index fa808dd0..3b812d45 100644 --- a/src/kernel/resources/invocations.py +++ b/src/kernel/resources/invocations.py @@ -37,7 +37,7 @@ def with_raw_response(self) -> InvocationsResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return InvocationsResourceWithRawResponse(self) @@ -46,7 +46,7 @@ def with_streaming_response(self) -> InvocationsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return InvocationsResourceWithStreamingResponse(self) @@ -355,7 +355,7 @@ def with_raw_response(self) -> AsyncInvocationsResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return AsyncInvocationsResourceWithRawResponse(self) @@ -364,7 +364,7 @@ def with_streaming_response(self) -> AsyncInvocationsResourceWithStreamingRespon """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return AsyncInvocationsResourceWithStreamingResponse(self) diff --git a/src/kernel/resources/profiles.py b/src/kernel/resources/profiles.py index 8d51da38..86064d52 100644 --- a/src/kernel/resources/profiles.py +++ b/src/kernel/resources/profiles.py @@ -37,7 +37,7 @@ def with_raw_response(self) -> ProfilesResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return ProfilesResourceWithRawResponse(self) @@ -46,7 +46,7 @@ def with_streaming_response(self) -> ProfilesResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return ProfilesResourceWithStreamingResponse(self) @@ -215,7 +215,7 @@ def with_raw_response(self) -> AsyncProfilesResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return AsyncProfilesResourceWithRawResponse(self) @@ -224,7 +224,7 @@ def with_streaming_response(self) -> AsyncProfilesResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return AsyncProfilesResourceWithStreamingResponse(self) diff --git a/src/kernel/resources/proxies.py b/src/kernel/resources/proxies.py index 4908ab79..6574a256 100644 --- a/src/kernel/resources/proxies.py +++ b/src/kernel/resources/proxies.py @@ -33,7 +33,7 @@ def with_raw_response(self) -> ProxiesResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return ProxiesResourceWithRawResponse(self) @@ -42,7 +42,7 @@ def with_streaming_response(self) -> ProxiesResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return ProxiesResourceWithStreamingResponse(self) @@ -226,7 +226,7 @@ def with_raw_response(self) -> AsyncProxiesResourceWithRawResponse: 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/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers """ return AsyncProxiesResourceWithRawResponse(self) @@ -235,7 +235,7 @@ def with_streaming_response(self) -> AsyncProxiesResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response """ return AsyncProxiesResourceWithStreamingResponse(self) From 4106d3c1825191f9e248ce5967cfc401d355643b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 20:40:41 +0000 Subject: [PATCH 252/448] feat: add MFA options to agent authentication workflow --- .stats.yml | 6 +- README.md | 2 +- src/kernel/_client.py | 2 +- .../resources/agents/auth/invocations.py | 74 ++++++++++++++- .../agents/agent_auth_invocation_response.py | 24 ++++- .../agents/auth/invocation_submit_params.py | 11 ++- .../agents/auth/test_invocations.py | 92 +++++++++++++++++++ tests/test_client.py | 4 +- 8 files changed, 201 insertions(+), 14 deletions(-) diff --git a/.stats.yml b/.stats.yml index 9ab4346b..ce759c07 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 89 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-8d66dbedea5b240936b338809f272568ca84a452fc13dbda835479f2ec068b41.yml -openapi_spec_hash: 7c499bfce2e996f1fff5e7791cea390e -config_hash: 2ee8c7057fa9b05cd0dabd23247c40ec +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-8e4a29d23d2882fcb0864606091790fd58bffa4f5d5c8d081052b72ad47b215b.yml +openapi_spec_hash: fc82d930dad739ac01e3c2bddba7bf61 +config_hash: 3a5e36dfb245210cfd978f679b3641d2 diff --git a/README.md b/README.md index d3e4341e..2826e5cd 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ It is generated with [Stainless](https://www.stainless.com/). ## Documentation -The REST API documentation can be found on [docs.onkernel.com](https://docs.onkernel.com). The full API of this library can be found in [api.md](api.md). +The REST API documentation can be found on [docs.kernel.com](https://docs.kernel.com). The full API of this library can be found in [api.md](api.md). ## Installation diff --git a/src/kernel/_client.py b/src/kernel/_client.py index 166ecdb2..79410d08 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -67,7 +67,7 @@ ] ENVIRONMENTS: Dict[str, str] = { - "production": "https://api.onkernel.com/", + "production": "https://api.kernel.com/", "development": "https://localhost:3001/", } diff --git a/src/kernel/resources/agents/auth/invocations.py b/src/kernel/resources/agents/auth/invocations.py index aa1c4da7..fcd5e3a6 100644 --- a/src/kernel/resources/agents/auth/invocations.py +++ b/src/kernel/resources/agents/auth/invocations.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Dict -from typing_extensions import overload +from typing_extensions import Literal, overload import httpx @@ -233,13 +233,46 @@ def submit( """ ... - @required_args(["field_values"], ["sso_button"]) + @overload + def submit( + self, + invocation_id: str, + *, + selected_mfa_type: Literal["sms", "call", "email", "totp", "push", "security_key"], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via 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, + ) -> AgentAuthSubmitResponse: + """Submits field values for the discovered login form. + + Returns immediately after + submission is accepted. Poll the invocation endpoint to track progress and get + results. + + Args: + selected_mfa_type: The MFA delivery method type + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @required_args(["field_values"], ["sso_button"], ["selected_mfa_type"]) def submit( self, invocation_id: str, *, field_values: Dict[str, str] | Omit = omit, sso_button: str | Omit = omit, + selected_mfa_type: Literal["sms", "call", "email", "totp", "push", "security_key"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -255,6 +288,7 @@ def submit( { "field_values": field_values, "sso_button": sso_button, + "selected_mfa_type": selected_mfa_type, }, invocation_submit_params.InvocationSubmitParams, ), @@ -471,13 +505,46 @@ async def submit( """ ... - @required_args(["field_values"], ["sso_button"]) + @overload + async def submit( + self, + invocation_id: str, + *, + selected_mfa_type: Literal["sms", "call", "email", "totp", "push", "security_key"], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via 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, + ) -> AgentAuthSubmitResponse: + """Submits field values for the discovered login form. + + Returns immediately after + submission is accepted. Poll the invocation endpoint to track progress and get + results. + + Args: + selected_mfa_type: The MFA delivery method type + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @required_args(["field_values"], ["sso_button"], ["selected_mfa_type"]) async def submit( self, invocation_id: str, *, field_values: Dict[str, str] | Omit = omit, sso_button: str | Omit = omit, + selected_mfa_type: Literal["sms", "call", "email", "totp", "push", "security_key"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -493,6 +560,7 @@ async def submit( { "field_values": field_values, "sso_button": sso_button, + "selected_mfa_type": selected_mfa_type, }, invocation_submit_params.InvocationSubmitParams, ), diff --git a/src/kernel/types/agents/agent_auth_invocation_response.py b/src/kernel/types/agents/agent_auth_invocation_response.py index 42b54a4c..2731290e 100644 --- a/src/kernel/types/agents/agent_auth_invocation_response.py +++ b/src/kernel/types/agents/agent_auth_invocation_response.py @@ -7,7 +7,23 @@ from ..._models import BaseModel from .discovered_field import DiscoveredField -__all__ = ["AgentAuthInvocationResponse", "PendingSSOButton"] +__all__ = ["AgentAuthInvocationResponse", "MfaOption", "PendingSSOButton"] + + +class MfaOption(BaseModel): + """An MFA method option for verification""" + + label: str + """The visible option text""" + + type: Literal["sms", "call", "email", "totp", "push", "security_key"] + """The MFA delivery method type""" + + description: Optional[str] = None + """Additional instructions from the site""" + + target: Optional[str] = None + """The masked destination (phone/email) if shown""" class PendingSSOButton(BaseModel): @@ -63,6 +79,12 @@ class AgentAuthInvocationResponse(BaseModel): live_view_url: Optional[str] = None """Browser live view URL for debugging the invocation""" + mfa_options: Optional[List[MfaOption]] = None + """ + MFA method options to choose from (present when step=awaiting_input and MFA + selection is required) + """ + pending_fields: Optional[List[DiscoveredField]] = None """Fields currently awaiting input (present when step=awaiting_input)""" diff --git a/src/kernel/types/agents/auth/invocation_submit_params.py b/src/kernel/types/agents/auth/invocation_submit_params.py index ad9f9c18..7a9c5aca 100644 --- a/src/kernel/types/agents/auth/invocation_submit_params.py +++ b/src/kernel/types/agents/auth/invocation_submit_params.py @@ -3,9 +3,9 @@ from __future__ import annotations from typing import Dict, Union -from typing_extensions import Required, TypeAlias, TypedDict +from typing_extensions import Literal, Required, TypeAlias, TypedDict -__all__ = ["InvocationSubmitParams", "Variant0", "Variant1"] +__all__ = ["InvocationSubmitParams", "Variant0", "Variant1", "Variant2"] class Variant0(TypedDict, total=False): @@ -18,4 +18,9 @@ class Variant1(TypedDict, total=False): """Selector of SSO button to click""" -InvocationSubmitParams: TypeAlias = Union[Variant0, Variant1] +class Variant2(TypedDict, total=False): + selected_mfa_type: Required[Literal["sms", "call", "email", "totp", "push", "security_key"]] + """The MFA delivery method type""" + + +InvocationSubmitParams: TypeAlias = Union[Variant0, Variant1, Variant2] diff --git a/tests/api_resources/agents/auth/test_invocations.py b/tests/api_resources/agents/auth/test_invocations.py index 1bae66da..6d70dfac 100644 --- a/tests/api_resources/agents/auth/test_invocations.py +++ b/tests/api_resources/agents/auth/test_invocations.py @@ -255,6 +255,52 @@ def test_path_params_submit_overload_2(self, client: Kernel) -> None: sso_button="xpath=//button[contains(text(), 'Continue with Google')]", ) + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_submit_overload_3(self, client: Kernel) -> None: + invocation = client.agents.auth.invocations.submit( + invocation_id="invocation_id", + selected_mfa_type="sms", + ) + assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_submit_overload_3(self, client: Kernel) -> None: + response = client.agents.auth.invocations.with_raw_response.submit( + invocation_id="invocation_id", + selected_mfa_type="sms", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + invocation = response.parse() + assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_submit_overload_3(self, client: Kernel) -> None: + with client.agents.auth.invocations.with_streaming_response.submit( + invocation_id="invocation_id", + selected_mfa_type="sms", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + invocation = response.parse() + assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_submit_overload_3(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): + client.agents.auth.invocations.with_raw_response.submit( + invocation_id="", + selected_mfa_type="sms", + ) + class TestAsyncInvocations: parametrize = pytest.mark.parametrize( @@ -495,3 +541,49 @@ async def test_path_params_submit_overload_2(self, async_client: AsyncKernel) -> invocation_id="", sso_button="xpath=//button[contains(text(), 'Continue with Google')]", ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_submit_overload_3(self, async_client: AsyncKernel) -> None: + invocation = await async_client.agents.auth.invocations.submit( + invocation_id="invocation_id", + selected_mfa_type="sms", + ) + assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_submit_overload_3(self, async_client: AsyncKernel) -> None: + response = await async_client.agents.auth.invocations.with_raw_response.submit( + invocation_id="invocation_id", + selected_mfa_type="sms", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + invocation = await response.parse() + assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_submit_overload_3(self, async_client: AsyncKernel) -> None: + async with async_client.agents.auth.invocations.with_streaming_response.submit( + invocation_id="invocation_id", + selected_mfa_type="sms", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + invocation = await response.parse() + assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_submit_overload_3(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): + await async_client.agents.auth.invocations.with_raw_response.submit( + invocation_id="", + selected_mfa_type="sms", + ) diff --git a/tests/test_client.py b/tests/test_client.py index 95661e8a..5eace697 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -578,7 +578,7 @@ def test_base_url_env(self) -> None: Kernel(api_key=api_key, _strict_response_validation=True, environment="production") client = Kernel(base_url=None, api_key=api_key, _strict_response_validation=True, environment="production") - assert str(client.base_url).startswith("https://api.onkernel.com/") + assert str(client.base_url).startswith("https://api.kernel.com/") client.close() @@ -1413,7 +1413,7 @@ async def test_base_url_env(self) -> None: client = AsyncKernel( base_url=None, api_key=api_key, _strict_response_validation=True, environment="production" ) - assert str(client.base_url).startswith("https://api.onkernel.com/") + assert str(client.base_url).startswith("https://api.kernel.com/") await client.close() From e8dfb20e935416b44875536d841821fe0bd80762 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 05:26:12 +0000 Subject: [PATCH 253/448] feat(client): add support for binary request streaming --- src/kernel/_base_client.py | 145 +++++++++++++++++-- src/kernel/_models.py | 17 ++- src/kernel/_types.py | 9 ++ src/kernel/resources/browsers/fs/fs.py | 24 +++- tests/test_client.py | 187 ++++++++++++++++++++++++- 5 files changed, 363 insertions(+), 19 deletions(-) diff --git a/src/kernel/_base_client.py b/src/kernel/_base_client.py index 787be54c..07809c5b 100644 --- a/src/kernel/_base_client.py +++ b/src/kernel/_base_client.py @@ -9,6 +9,7 @@ import inspect import logging import platform +import warnings import email.utils from types import TracebackType from random import random @@ -51,9 +52,11 @@ ResponseT, AnyMapping, PostParser, + BinaryTypes, RequestFiles, HttpxSendArgs, RequestOptions, + AsyncBinaryTypes, HttpxRequestFiles, ModelBuilderProtocol, not_given, @@ -477,8 +480,19 @@ def _build_request( retries_taken: int = 0, ) -> httpx.Request: if log.isEnabledFor(logging.DEBUG): - log.debug("Request options: %s", model_dump(options, exclude_unset=True)) - + log.debug( + "Request options: %s", + model_dump( + options, + exclude_unset=True, + # Pydantic v1 can't dump every type we support in content, so we exclude it for now. + exclude={ + "content", + } + if PYDANTIC_V1 + else {}, + ), + ) kwargs: dict[str, Any] = {} json_data = options.json_data @@ -532,7 +546,13 @@ def _build_request( is_body_allowed = options.method.lower() != "get" if is_body_allowed: - if isinstance(json_data, bytes): + if options.content is not None and json_data is not None: + raise TypeError("Passing both `content` and `json_data` is not supported") + if options.content is not None and files is not None: + raise TypeError("Passing both `content` and `files` is not supported") + if options.content is not None: + kwargs["content"] = options.content + elif isinstance(json_data, bytes): kwargs["content"] = json_data else: kwargs["json"] = json_data if is_given(json_data) else None @@ -1194,6 +1214,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[False] = False, @@ -1206,6 +1227,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[True], @@ -1219,6 +1241,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: bool, @@ -1231,13 +1254,25 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: bool = False, stream_cls: type[_StreamT] | None = None, ) -> ResponseT | _StreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=to_httpx_files(files), **options + method="post", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) @@ -1247,11 +1282,23 @@ def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + method="patch", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return self.request(cast_to, opts) @@ -1261,11 +1308,23 @@ def put( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=to_httpx_files(files), **options + method="put", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return self.request(cast_to, opts) @@ -1275,9 +1334,19 @@ def delete( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) return self.request(cast_to, opts) def get_api_list( @@ -1717,6 +1786,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[False] = False, @@ -1729,6 +1799,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[True], @@ -1742,6 +1813,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: bool, @@ -1754,13 +1826,25 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: bool = False, stream_cls: type[_AsyncStreamT] | None = None, ) -> ResponseT | _AsyncStreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="post", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) @@ -1770,11 +1854,28 @@ async def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="patch", + url=path, + json_data=body, + content=content, + files=await async_to_httpx_files(files), + **options, ) return await self.request(cast_to, opts) @@ -1784,11 +1885,23 @@ async def put( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="put", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts) @@ -1798,9 +1911,19 @@ async def delete( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) return await self.request(cast_to, opts) def get_api_list( diff --git a/src/kernel/_models.py b/src/kernel/_models.py index ca9500b2..29070e05 100644 --- a/src/kernel/_models.py +++ b/src/kernel/_models.py @@ -3,7 +3,20 @@ import os import inspect import weakref -from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast +from typing import ( + IO, + TYPE_CHECKING, + Any, + Type, + Union, + Generic, + TypeVar, + Callable, + Iterable, + Optional, + AsyncIterable, + cast, +) from datetime import date, datetime from typing_extensions import ( List, @@ -787,6 +800,7 @@ class FinalRequestOptionsInput(TypedDict, total=False): timeout: float | Timeout | None files: HttpxRequestFiles | None idempotency_key: str + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] json_data: Body extra_json: AnyMapping follow_redirects: bool @@ -805,6 +819,7 @@ class FinalRequestOptions(pydantic.BaseModel): post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() follow_redirects: Union[bool, None] = None + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None # It should be noted that we cannot use `json` here as that would override # a BaseModel method in an incompatible fashion. json_data: Union[Body, None] = None diff --git a/src/kernel/_types.py b/src/kernel/_types.py index 275ffbbc..28254f95 100644 --- a/src/kernel/_types.py +++ b/src/kernel/_types.py @@ -13,9 +13,11 @@ Mapping, TypeVar, Callable, + Iterable, Iterator, Optional, Sequence, + AsyncIterable, ) from typing_extensions import ( Set, @@ -56,6 +58,13 @@ else: Base64FileInput = Union[IO[bytes], PathLike] FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8. + + +# Used for sending raw binary data / streaming data in request bodies +# e.g. for file uploads without multipart encoding +BinaryTypes = Union[bytes, bytearray, IO[bytes], Iterable[bytes]] +AsyncBinaryTypes = Union[bytes, bytearray, IO[bytes], AsyncIterable[bytes]] + FileTypes = Union[ # file (or bytes) FileContent, diff --git a/src/kernel/resources/browsers/fs/fs.py b/src/kernel/resources/browsers/fs/fs.py index 0da0bddd..3501a2a6 100644 --- a/src/kernel/resources/browsers/fs/fs.py +++ b/src/kernel/resources/browsers/fs/fs.py @@ -2,6 +2,7 @@ from __future__ import annotations +import os from typing import Mapping, Iterable, cast import httpx @@ -15,7 +16,20 @@ AsyncWatchResourceWithStreamingResponse, ) from ...._files import read_file_content, async_read_file_content -from ...._types import Body, Omit, Query, Headers, NoneType, NotGiven, FileTypes, FileContent, omit, not_given +from ...._types import ( + Body, + Omit, + Query, + Headers, + NoneType, + NotGiven, + FileTypes, + BinaryTypes, + FileContent, + AsyncBinaryTypes, + omit, + not_given, +) from ...._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform from ...._compat import cached_property from ...._resource import SyncAPIResource, AsyncAPIResource @@ -562,7 +576,7 @@ def upload_zip( def write_file( self, id: str, - contents: FileContent, + contents: FileContent | BinaryTypes, *, path: str, mode: str | Omit = omit, @@ -595,7 +609,7 @@ def write_file( extra_headers["Content-Type"] = "application/octet-stream" return self._put( f"/browsers/{id}/fs/write_file", - body=read_file_content(contents), + content=read_file_content(contents) if isinstance(contents, os.PathLike) else contents, options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -1121,7 +1135,7 @@ async def upload_zip( async def write_file( self, id: str, - contents: FileContent, + contents: FileContent | AsyncBinaryTypes, *, path: str, mode: str | Omit = omit, @@ -1154,7 +1168,7 @@ async def write_file( extra_headers["Content-Type"] = "application/octet-stream" return await self._put( f"/browsers/{id}/fs/write_file", - body=await async_read_file_content(contents), + content=await async_read_file_content(contents) if isinstance(contents, os.PathLike) else contents, options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, diff --git a/tests/test_client.py b/tests/test_client.py index 5eace697..e52baa48 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -8,10 +8,11 @@ import json import asyncio import inspect +import dataclasses import tracemalloc -from typing import Any, Union, cast +from typing import Any, Union, TypeVar, Callable, Iterable, Iterator, Optional, Coroutine, cast from unittest import mock -from typing_extensions import Literal +from typing_extensions import Literal, AsyncIterator, override import httpx import pytest @@ -36,6 +37,7 @@ from .utils import update_env +T = TypeVar("T") base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") api_key = "My API Key" @@ -50,6 +52,57 @@ def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float: return 0.1 +def mirror_request_content(request: httpx.Request) -> httpx.Response: + return httpx.Response(200, content=request.content) + + +# note: we can't use the httpx.MockTransport class as it consumes the request +# body itself, which means we can't test that the body is read lazily +class MockTransport(httpx.BaseTransport, httpx.AsyncBaseTransport): + def __init__( + self, + handler: Callable[[httpx.Request], httpx.Response] + | Callable[[httpx.Request], Coroutine[Any, Any, httpx.Response]], + ) -> None: + self.handler = handler + + @override + def handle_request( + self, + request: httpx.Request, + ) -> httpx.Response: + assert not inspect.iscoroutinefunction(self.handler), "handler must not be a coroutine function" + assert inspect.isfunction(self.handler), "handler must be a function" + return self.handler(request) + + @override + async def handle_async_request( + self, + request: httpx.Request, + ) -> httpx.Response: + assert inspect.iscoroutinefunction(self.handler), "handler must be a coroutine function" + return await self.handler(request) + + +@dataclasses.dataclass +class Counter: + value: int = 0 + + +def _make_sync_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> Iterator[T]: + for item in iterable: + if counter: + counter.value += 1 + yield item + + +async def _make_async_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> AsyncIterator[T]: + for item in iterable: + if counter: + counter.value += 1 + yield item + + def _get_open_connections(client: Kernel | AsyncKernel) -> int: transport = client._client._transport assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) @@ -500,6 +553,70 @@ def test_multipart_repeating_array(self, client: Kernel) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload(self, respx_mock: MockRouter, client: Kernel) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + response = client.post( + "/upload", + content=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + def test_binary_content_upload_with_iterator(self) -> None: + file_content = b"Hello, this is a test file." + counter = Counter() + iterator = _make_sync_iterator([file_content], counter=counter) + + def mock_handler(request: httpx.Request) -> httpx.Response: + assert counter.value == 0, "the request body should not have been read" + return httpx.Response(200, content=request.read()) + + with Kernel( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.Client(transport=MockTransport(handler=mock_handler)), + ) as client: + response = client.post( + "/upload", + content=iterator, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + assert counter.value == 1 + + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload_with_body_is_deprecated(self, respx_mock: MockRouter, client: Kernel) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + with pytest.deprecated_call( + match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead." + ): + response = client.post( + "/upload", + body=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + @pytest.mark.respx(base_url=base_url) def test_basic_union_response(self, respx_mock: MockRouter, client: Kernel) -> None: class Model1(BaseModel): @@ -1329,6 +1446,72 @@ def test_multipart_repeating_array(self, async_client: AsyncKernel) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload(self, respx_mock: MockRouter, async_client: AsyncKernel) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + response = await async_client.post( + "/upload", + content=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + async def test_binary_content_upload_with_asynciterator(self) -> None: + file_content = b"Hello, this is a test file." + counter = Counter() + iterator = _make_async_iterator([file_content], counter=counter) + + async def mock_handler(request: httpx.Request) -> httpx.Response: + assert counter.value == 0, "the request body should not have been read" + return httpx.Response(200, content=await request.aread()) + + async with AsyncKernel( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.AsyncClient(transport=MockTransport(handler=mock_handler)), + ) as client: + response = await client.post( + "/upload", + content=iterator, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + assert counter.value == 1 + + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload_with_body_is_deprecated( + self, respx_mock: MockRouter, async_client: AsyncKernel + ) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + with pytest.deprecated_call( + match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead." + ): + response = await async_client.post( + "/upload", + body=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + @pytest.mark.respx(base_url=base_url) async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncKernel) -> None: class Model1(BaseModel): From aea9365c69c6e5f2a298a662ac55694e7466a96d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 15:19:41 +0000 Subject: [PATCH 254/448] feat(api): manual updates Updated TypeScript package name and API URL to correct values. --- .stats.yml | 2 +- src/kernel/_client.py | 2 +- tests/test_client.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.stats.yml b/.stats.yml index ce759c07..8bd6c680 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 89 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-8e4a29d23d2882fcb0864606091790fd58bffa4f5d5c8d081052b72ad47b215b.yml openapi_spec_hash: fc82d930dad739ac01e3c2bddba7bf61 -config_hash: 3a5e36dfb245210cfd978f679b3641d2 +config_hash: d0585c44724cd7deecea60a5ffae69df diff --git a/src/kernel/_client.py b/src/kernel/_client.py index 79410d08..166ecdb2 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -67,7 +67,7 @@ ] ENVIRONMENTS: Dict[str, str] = { - "production": "https://api.kernel.com/", + "production": "https://api.onkernel.com/", "development": "https://localhost:3001/", } diff --git a/tests/test_client.py b/tests/test_client.py index e52baa48..4d3fb09a 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -695,7 +695,7 @@ def test_base_url_env(self) -> None: Kernel(api_key=api_key, _strict_response_validation=True, environment="production") client = Kernel(base_url=None, api_key=api_key, _strict_response_validation=True, environment="production") - assert str(client.base_url).startswith("https://api.kernel.com/") + assert str(client.base_url).startswith("https://api.onkernel.com/") client.close() @@ -1596,7 +1596,7 @@ async def test_base_url_env(self) -> None: client = AsyncKernel( base_url=None, api_key=api_key, _strict_response_validation=True, environment="production" ) - assert str(client.base_url).startswith("https://api.kernel.com/") + assert str(client.base_url).startswith("https://api.onkernel.com/") await client.close() From c17e743386cd184c9c642eceeb2fa8093f568ad0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 23:58:30 +0000 Subject: [PATCH 255/448] feat: add WebSocket process attach and PTY support --- .stats.yml | 8 +- api.md | 2 + src/kernel/resources/browsers/process.py | 143 +++++++++++++++++- src/kernel/types/browsers/__init__.py | 2 + .../types/browsers/process_resize_params.py | 17 +++ .../types/browsers/process_resize_response.py | 12 ++ .../types/browsers/process_spawn_params.py | 9 ++ tests/api_resources/browsers/test_process.py | 131 ++++++++++++++++ 8 files changed, 319 insertions(+), 5 deletions(-) create mode 100644 src/kernel/types/browsers/process_resize_params.py create mode 100644 src/kernel/types/browsers/process_resize_response.py diff --git a/.stats.yml b/.stats.yml index 8bd6c680..e7a094fa 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 89 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-8e4a29d23d2882fcb0864606091790fd58bffa4f5d5c8d081052b72ad47b215b.yml -openapi_spec_hash: fc82d930dad739ac01e3c2bddba7bf61 -config_hash: d0585c44724cd7deecea60a5ffae69df +configured_endpoints: 90 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-6a44c851ec955b997558a8524eb641355ff3097474f40772b8ea2fef5bee4134.yml +openapi_spec_hash: 155ee005a1b43e1c11e843de91e9f509 +config_hash: 6cbbf855a29bc675f35ddb1106ea9083 diff --git a/api.md b/api.md index db5f2dfb..f975c67a 100644 --- a/api.md +++ b/api.md @@ -154,6 +154,7 @@ Types: from kernel.types.browsers import ( ProcessExecResponse, ProcessKillResponse, + ProcessResizeResponse, ProcessSpawnResponse, ProcessStatusResponse, ProcessStdinResponse, @@ -165,6 +166,7 @@ Methods: - client.browsers.process.exec(id, \*\*params) -> ProcessExecResponse - client.browsers.process.kill(process_id, \*, id, \*\*params) -> ProcessKillResponse +- client.browsers.process.resize(process_id, \*, id, \*\*params) -> ProcessResizeResponse - client.browsers.process.spawn(id, \*\*params) -> ProcessSpawnResponse - client.browsers.process.status(process_id, \*, id) -> ProcessStatusResponse - client.browsers.process.stdin(process_id, \*, id, \*\*params) -> ProcessStdinResponse diff --git a/src/kernel/resources/browsers/process.py b/src/kernel/resources/browsers/process.py index f5c4341e..9932f40c 100644 --- a/src/kernel/resources/browsers/process.py +++ b/src/kernel/resources/browsers/process.py @@ -19,11 +19,18 @@ ) from ..._streaming import Stream, AsyncStream from ..._base_client import make_request_options -from ...types.browsers import process_exec_params, process_kill_params, process_spawn_params, process_stdin_params +from ...types.browsers import ( + process_exec_params, + process_kill_params, + process_spawn_params, + process_stdin_params, + process_resize_params, +) from ...types.browsers.process_exec_response import ProcessExecResponse from ...types.browsers.process_kill_response import ProcessKillResponse from ...types.browsers.process_spawn_response import ProcessSpawnResponse from ...types.browsers.process_stdin_response import ProcessStdinResponse +from ...types.browsers.process_resize_response import ProcessResizeResponse from ...types.browsers.process_status_response import ProcessStatusResponse from ...types.browsers.process_stdout_stream_response import ProcessStdoutStreamResponse @@ -156,16 +163,68 @@ def kill( cast_to=ProcessKillResponse, ) + def resize( + self, + process_id: str, + *, + id: str, + cols: int, + rows: 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, + ) -> ProcessResizeResponse: + """ + Resize a PTY-backed process terminal + + Args: + cols: New terminal columns. + + rows: New terminal rows. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not process_id: + raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}") + return self._post( + f"/browsers/{id}/process/{process_id}/resize", + body=maybe_transform( + { + "cols": cols, + "rows": rows, + }, + process_resize_params.ProcessResizeParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProcessResizeResponse, + ) + def spawn( self, id: str, *, command: str, + allocate_tty: bool | Omit = omit, args: SequenceNotStr[str] | Omit = omit, as_root: bool | Omit = omit, as_user: Optional[str] | Omit = omit, + cols: int | Omit = omit, cwd: Optional[str] | Omit = omit, env: Dict[str, str] | Omit = omit, + rows: int | Omit = omit, timeout_sec: Optional[int] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -180,16 +239,22 @@ def spawn( Args: command: Executable or shell command to run. + allocate_tty: Allocate a pseudo-terminal (PTY) for interactive shells. + args: Command arguments. as_root: Run the process with root privileges. as_user: Run the process as this user. + cols: Initial terminal columns. Only used when allocate_tty is true. + cwd: Working directory (absolute path) to run the command in. env: Environment variables to set for the process. + rows: Initial terminal rows. Only used when allocate_tty is true. + timeout_sec: Maximum execution time in seconds. extra_headers: Send extra headers @@ -207,11 +272,14 @@ def spawn( body=maybe_transform( { "command": command, + "allocate_tty": allocate_tty, "args": args, "as_root": as_root, "as_user": as_user, + "cols": cols, "cwd": cwd, "env": env, + "rows": rows, "timeout_sec": timeout_sec, }, process_spawn_params.ProcessSpawnParams, @@ -464,16 +532,68 @@ async def kill( cast_to=ProcessKillResponse, ) + async def resize( + self, + process_id: str, + *, + id: str, + cols: int, + rows: 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, + ) -> ProcessResizeResponse: + """ + Resize a PTY-backed process terminal + + Args: + cols: New terminal columns. + + rows: New terminal rows. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not process_id: + raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}") + return await self._post( + f"/browsers/{id}/process/{process_id}/resize", + body=await async_maybe_transform( + { + "cols": cols, + "rows": rows, + }, + process_resize_params.ProcessResizeParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProcessResizeResponse, + ) + async def spawn( self, id: str, *, command: str, + allocate_tty: bool | Omit = omit, args: SequenceNotStr[str] | Omit = omit, as_root: bool | Omit = omit, as_user: Optional[str] | Omit = omit, + cols: int | Omit = omit, cwd: Optional[str] | Omit = omit, env: Dict[str, str] | Omit = omit, + rows: int | Omit = omit, timeout_sec: Optional[int] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -488,16 +608,22 @@ async def spawn( Args: command: Executable or shell command to run. + allocate_tty: Allocate a pseudo-terminal (PTY) for interactive shells. + args: Command arguments. as_root: Run the process with root privileges. as_user: Run the process as this user. + cols: Initial terminal columns. Only used when allocate_tty is true. + cwd: Working directory (absolute path) to run the command in. env: Environment variables to set for the process. + rows: Initial terminal rows. Only used when allocate_tty is true. + timeout_sec: Maximum execution time in seconds. extra_headers: Send extra headers @@ -515,11 +641,14 @@ async def spawn( body=await async_maybe_transform( { "command": command, + "allocate_tty": allocate_tty, "args": args, "as_root": as_root, "as_user": as_user, + "cols": cols, "cwd": cwd, "env": env, + "rows": rows, "timeout_sec": timeout_sec, }, process_spawn_params.ProcessSpawnParams, @@ -656,6 +785,9 @@ def __init__(self, process: ProcessResource) -> None: self.kill = to_raw_response_wrapper( process.kill, ) + self.resize = to_raw_response_wrapper( + process.resize, + ) self.spawn = to_raw_response_wrapper( process.spawn, ) @@ -680,6 +812,9 @@ def __init__(self, process: AsyncProcessResource) -> None: self.kill = async_to_raw_response_wrapper( process.kill, ) + self.resize = async_to_raw_response_wrapper( + process.resize, + ) self.spawn = async_to_raw_response_wrapper( process.spawn, ) @@ -704,6 +839,9 @@ def __init__(self, process: ProcessResource) -> None: self.kill = to_streamed_response_wrapper( process.kill, ) + self.resize = to_streamed_response_wrapper( + process.resize, + ) self.spawn = to_streamed_response_wrapper( process.spawn, ) @@ -728,6 +866,9 @@ def __init__(self, process: AsyncProcessResource) -> None: self.kill = async_to_streamed_response_wrapper( process.kill, ) + self.resize = async_to_streamed_response_wrapper( + process.resize, + ) self.spawn = async_to_streamed_response_wrapper( process.spawn, ) diff --git a/src/kernel/types/browsers/__init__.py b/src/kernel/types/browsers/__init__.py index 546fdc64..e6b2eca3 100644 --- a/src/kernel/types/browsers/__init__.py +++ b/src/kernel/types/browsers/__init__.py @@ -21,10 +21,12 @@ from .f_list_files_response import FListFilesResponse as FListFilesResponse from .process_exec_response import ProcessExecResponse as ProcessExecResponse from .process_kill_response import ProcessKillResponse as ProcessKillResponse +from .process_resize_params import ProcessResizeParams as ProcessResizeParams from .replay_start_response import ReplayStartResponse as ReplayStartResponse from .computer_scroll_params import ComputerScrollParams as ComputerScrollParams from .process_spawn_response import ProcessSpawnResponse as ProcessSpawnResponse from .process_stdin_response import ProcessStdinResponse as ProcessStdinResponse +from .process_resize_response import ProcessResizeResponse as ProcessResizeResponse from .process_status_response import ProcessStatusResponse as ProcessStatusResponse from .computer_press_key_params import ComputerPressKeyParams as ComputerPressKeyParams from .computer_type_text_params import ComputerTypeTextParams as ComputerTypeTextParams diff --git a/src/kernel/types/browsers/process_resize_params.py b/src/kernel/types/browsers/process_resize_params.py new file mode 100644 index 00000000..4fdb8ad5 --- /dev/null +++ b/src/kernel/types/browsers/process_resize_params.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["ProcessResizeParams"] + + +class ProcessResizeParams(TypedDict, total=False): + id: Required[str] + + cols: Required[int] + """New terminal columns.""" + + rows: Required[int] + """New terminal rows.""" diff --git a/src/kernel/types/browsers/process_resize_response.py b/src/kernel/types/browsers/process_resize_response.py new file mode 100644 index 00000000..6997517d --- /dev/null +++ b/src/kernel/types/browsers/process_resize_response.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from ..._models import BaseModel + +__all__ = ["ProcessResizeResponse"] + + +class ProcessResizeResponse(BaseModel): + """Generic OK response.""" + + ok: bool + """Indicates success.""" diff --git a/src/kernel/types/browsers/process_spawn_params.py b/src/kernel/types/browsers/process_spawn_params.py index 8e901cb0..78d57a2d 100644 --- a/src/kernel/types/browsers/process_spawn_params.py +++ b/src/kernel/types/browsers/process_spawn_params.py @@ -14,6 +14,9 @@ class ProcessSpawnParams(TypedDict, total=False): command: Required[str] """Executable or shell command to run.""" + allocate_tty: bool + """Allocate a pseudo-terminal (PTY) for interactive shells.""" + args: SequenceNotStr[str] """Command arguments.""" @@ -23,11 +26,17 @@ class ProcessSpawnParams(TypedDict, total=False): as_user: Optional[str] """Run the process as this user.""" + cols: int + """Initial terminal columns. Only used when allocate_tty is true.""" + cwd: Optional[str] """Working directory (absolute path) to run the command in.""" env: Dict[str, str] """Environment variables to set for the process.""" + rows: int + """Initial terminal rows. Only used when allocate_tty is true.""" + timeout_sec: Optional[int] """Maximum execution time in seconds.""" diff --git a/tests/api_resources/browsers/test_process.py b/tests/api_resources/browsers/test_process.py index 69977621..3c645fa4 100644 --- a/tests/api_resources/browsers/test_process.py +++ b/tests/api_resources/browsers/test_process.py @@ -14,6 +14,7 @@ ProcessKillResponse, ProcessSpawnResponse, ProcessStdinResponse, + ProcessResizeResponse, ProcessStatusResponse, ) @@ -141,6 +142,68 @@ def test_path_params_kill(self, client: Kernel) -> None: signal="TERM", ) + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_resize(self, client: Kernel) -> None: + process = client.browsers.process.resize( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + cols=1, + rows=1, + ) + assert_matches_type(ProcessResizeResponse, process, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_resize(self, client: Kernel) -> None: + response = client.browsers.process.with_raw_response.resize( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + cols=1, + rows=1, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + process = response.parse() + assert_matches_type(ProcessResizeResponse, process, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_resize(self, client: Kernel) -> None: + with client.browsers.process.with_streaming_response.resize( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + cols=1, + rows=1, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + process = response.parse() + assert_matches_type(ProcessResizeResponse, process, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_resize(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.process.with_raw_response.resize( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="", + cols=1, + rows=1, + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `process_id` but received ''"): + client.browsers.process.with_raw_response.resize( + process_id="", + id="id", + cols=1, + rows=1, + ) + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_spawn(self, client: Kernel) -> None: @@ -156,11 +219,14 @@ def test_method_spawn_with_all_params(self, client: Kernel) -> None: process = client.browsers.process.spawn( id="id", command="command", + allocate_tty=True, args=["string"], as_root=True, as_user="as_user", + cols=1, cwd="/J!", env={"foo": "string"}, + rows=1, timeout_sec=0, ) assert_matches_type(ProcessSpawnResponse, process, path=["response"]) @@ -486,6 +552,68 @@ async def test_path_params_kill(self, async_client: AsyncKernel) -> None: signal="TERM", ) + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_resize(self, async_client: AsyncKernel) -> None: + process = await async_client.browsers.process.resize( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + cols=1, + rows=1, + ) + assert_matches_type(ProcessResizeResponse, process, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_resize(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.process.with_raw_response.resize( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + cols=1, + rows=1, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + process = await response.parse() + assert_matches_type(ProcessResizeResponse, process, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_resize(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.process.with_streaming_response.resize( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="id", + cols=1, + rows=1, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + process = await response.parse() + assert_matches_type(ProcessResizeResponse, process, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_resize(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.process.with_raw_response.resize( + process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="", + cols=1, + rows=1, + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `process_id` but received ''"): + await async_client.browsers.process.with_raw_response.resize( + process_id="", + id="id", + cols=1, + rows=1, + ) + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_spawn(self, async_client: AsyncKernel) -> None: @@ -501,11 +629,14 @@ async def test_method_spawn_with_all_params(self, async_client: AsyncKernel) -> process = await async_client.browsers.process.spawn( id="id", command="command", + allocate_tty=True, args=["string"], as_root=True, as_user="as_user", + cols=1, cwd="/J!", env={"foo": "string"}, + rows=1, timeout_sec=0, ) assert_matches_type(ProcessSpawnResponse, process, path=["response"]) From 9f4ea27eb8d9bcf65522d578f599ecea5a33793e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 20:05:57 +0000 Subject: [PATCH 256/448] feat(api): add IP address logging for residential and custom proxies --- .stats.yml | 6 +++--- src/kernel/_client.py | 2 +- src/kernel/types/proxy_check_response.py | 3 +++ src/kernel/types/proxy_create_response.py | 3 +++ src/kernel/types/proxy_list_response.py | 3 +++ src/kernel/types/proxy_retrieve_response.py | 3 +++ tests/test_client.py | 4 ++-- 7 files changed, 18 insertions(+), 6 deletions(-) diff --git a/.stats.yml b/.stats.yml index e7a094fa..f0310d3b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 90 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-6a44c851ec955b997558a8524eb641355ff3097474f40772b8ea2fef5bee4134.yml -openapi_spec_hash: 155ee005a1b43e1c11e843de91e9f509 -config_hash: 6cbbf855a29bc675f35ddb1106ea9083 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-cc60c65c6bb0b8a8ea662cf758806e641790870ded35f6ffdb9f4801f3d29b15.yml +openapi_spec_hash: a1074e1bba578bcd5912512166ada0dc +config_hash: 7868d3397406c2974b6f058488aeefd6 diff --git a/src/kernel/_client.py b/src/kernel/_client.py index 166ecdb2..79410d08 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -67,7 +67,7 @@ ] ENVIRONMENTS: Dict[str, str] = { - "production": "https://api.onkernel.com/", + "production": "https://api.kernel.com/", "development": "https://localhost:3001/", } diff --git a/src/kernel/types/proxy_check_response.py b/src/kernel/types/proxy_check_response.py index dc45f4f5..26b6d0a1 100644 --- a/src/kernel/types/proxy_check_response.py +++ b/src/kernel/types/proxy_check_response.py @@ -182,6 +182,9 @@ class ProxyCheckResponse(BaseModel): config: Optional[Config] = None """Configuration specific to the selected proxy `type`.""" + ip_address: Optional[str] = None + """IP address that the proxy uses when making requests.""" + last_checked: Optional[datetime] = None """Timestamp of the last health check performed on this proxy.""" diff --git a/src/kernel/types/proxy_create_response.py b/src/kernel/types/proxy_create_response.py index dc474ab2..939ec4f7 100644 --- a/src/kernel/types/proxy_create_response.py +++ b/src/kernel/types/proxy_create_response.py @@ -182,6 +182,9 @@ class ProxyCreateResponse(BaseModel): config: Optional[Config] = None """Configuration specific to the selected proxy `type`.""" + ip_address: Optional[str] = None + """IP address that the proxy uses when making requests.""" + last_checked: Optional[datetime] = None """Timestamp of the last health check performed on this proxy.""" diff --git a/src/kernel/types/proxy_list_response.py b/src/kernel/types/proxy_list_response.py index 08c846f0..2d1ffb99 100644 --- a/src/kernel/types/proxy_list_response.py +++ b/src/kernel/types/proxy_list_response.py @@ -183,6 +183,9 @@ class ProxyListResponseItem(BaseModel): config: Optional[ProxyListResponseItemConfig] = None """Configuration specific to the selected proxy `type`.""" + ip_address: Optional[str] = None + """IP address that the proxy uses when making requests.""" + last_checked: Optional[datetime] = None """Timestamp of the last health check performed on this proxy.""" diff --git a/src/kernel/types/proxy_retrieve_response.py b/src/kernel/types/proxy_retrieve_response.py index 24c7b96f..bf99ed0e 100644 --- a/src/kernel/types/proxy_retrieve_response.py +++ b/src/kernel/types/proxy_retrieve_response.py @@ -182,6 +182,9 @@ class ProxyRetrieveResponse(BaseModel): config: Optional[Config] = None """Configuration specific to the selected proxy `type`.""" + ip_address: Optional[str] = None + """IP address that the proxy uses when making requests.""" + last_checked: Optional[datetime] = None """Timestamp of the last health check performed on this proxy.""" diff --git a/tests/test_client.py b/tests/test_client.py index 4d3fb09a..e52baa48 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -695,7 +695,7 @@ def test_base_url_env(self) -> None: Kernel(api_key=api_key, _strict_response_validation=True, environment="production") client = Kernel(base_url=None, api_key=api_key, _strict_response_validation=True, environment="production") - assert str(client.base_url).startswith("https://api.onkernel.com/") + assert str(client.base_url).startswith("https://api.kernel.com/") client.close() @@ -1596,7 +1596,7 @@ async def test_base_url_env(self) -> None: client = AsyncKernel( base_url=None, api_key=api_key, _strict_response_validation=True, environment="production" ) - assert str(client.base_url).startswith("https://api.onkernel.com/") + assert str(client.base_url).startswith("https://api.kernel.com/") await client.close() From 5636ce4e066d8d0e653311bd0f6598ea3e7d55c5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 16:15:42 +0000 Subject: [PATCH 257/448] feat: Support hot swap proxy on a session --- .stats.yml | 8 +- api.md | 2 + src/kernel/resources/browsers/browsers.py | 94 +++++++++++++++++- src/kernel/types/__init__.py | 2 + src/kernel/types/browser_update_params.py | 16 +++ src/kernel/types/browser_update_response.py | 64 ++++++++++++ tests/api_resources/test_browsers.py | 103 ++++++++++++++++++++ 7 files changed, 284 insertions(+), 5 deletions(-) create mode 100644 src/kernel/types/browser_update_params.py create mode 100644 src/kernel/types/browser_update_response.py diff --git a/.stats.yml b/.stats.yml index f0310d3b..330b531e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 90 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-cc60c65c6bb0b8a8ea662cf758806e641790870ded35f6ffdb9f4801f3d29b15.yml -openapi_spec_hash: a1074e1bba578bcd5912512166ada0dc -config_hash: 7868d3397406c2974b6f058488aeefd6 +configured_endpoints: 91 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-4f5307643555b7917e8681b1966ae0b99f770cf59805e2f917ec7528edf11ba8.yml +openapi_spec_hash: 873a9aa3a88b6cec1ad94f848eeb1c45 +config_hash: 20337f7888852c165d099faa7589c90a diff --git a/api.md b/api.md index f975c67a..51f8cdb4 100644 --- a/api.md +++ b/api.md @@ -81,6 +81,7 @@ from kernel.types import ( Profile, BrowserCreateResponse, BrowserRetrieveResponse, + BrowserUpdateResponse, BrowserListResponse, ) ``` @@ -89,6 +90,7 @@ Methods: - client.browsers.create(\*\*params) -> BrowserCreateResponse - client.browsers.retrieve(id) -> BrowserRetrieveResponse +- client.browsers.update(id, \*\*params) -> BrowserUpdateResponse - client.browsers.list(\*\*params) -> SyncOffsetPagination[BrowserListResponse] - client.browsers.delete(\*\*params) -> None - client.browsers.delete_by_id(id) -> None diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 8050a7dc..ab57854c 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -3,7 +3,7 @@ from __future__ import annotations import typing_extensions -from typing import Mapping, Iterable, cast +from typing import Mapping, Iterable, Optional, cast import httpx @@ -27,6 +27,7 @@ browser_list_params, browser_create_params, browser_delete_params, + browser_update_params, browser_load_extensions_params, ) from .process import ( @@ -75,6 +76,7 @@ from ..._base_client import AsyncPaginator, make_request_options from ...types.browser_list_response import BrowserListResponse from ...types.browser_create_response import BrowserCreateResponse +from ...types.browser_update_response import BrowserUpdateResponse from ...types.browser_persistence_param import BrowserPersistenceParam from ...types.browser_retrieve_response import BrowserRetrieveResponse from ...types.shared_params.browser_profile import BrowserProfile @@ -253,6 +255,45 @@ def retrieve( cast_to=BrowserRetrieveResponse, ) + def update( + self, + id: str, + *, + proxy_id: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> BrowserUpdateResponse: + """Update a browser session. + + Args: + proxy_id: ID of the proxy to use. + + Omit to leave unchanged, set to empty string to remove + proxy. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._patch( + f"/browsers/{id}", + body=maybe_transform({"proxy_id": proxy_id}, browser_update_params.BrowserUpdateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BrowserUpdateResponse, + ) + def list( self, *, @@ -598,6 +639,45 @@ async def retrieve( cast_to=BrowserRetrieveResponse, ) + async def update( + self, + id: str, + *, + proxy_id: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> BrowserUpdateResponse: + """Update a browser session. + + Args: + proxy_id: ID of the proxy to use. + + Omit to leave unchanged, set to empty string to remove + proxy. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._patch( + f"/browsers/{id}", + body=await async_maybe_transform({"proxy_id": proxy_id}, browser_update_params.BrowserUpdateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BrowserUpdateResponse, + ) + def list( self, *, @@ -786,6 +866,9 @@ def __init__(self, browsers: BrowsersResource) -> None: self.retrieve = to_raw_response_wrapper( browsers.retrieve, ) + self.update = to_raw_response_wrapper( + browsers.update, + ) self.list = to_raw_response_wrapper( browsers.list, ) @@ -836,6 +919,9 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None: self.retrieve = async_to_raw_response_wrapper( browsers.retrieve, ) + self.update = async_to_raw_response_wrapper( + browsers.update, + ) self.list = async_to_raw_response_wrapper( browsers.list, ) @@ -886,6 +972,9 @@ def __init__(self, browsers: BrowsersResource) -> None: self.retrieve = to_streamed_response_wrapper( browsers.retrieve, ) + self.update = to_streamed_response_wrapper( + browsers.update, + ) self.list = to_streamed_response_wrapper( browsers.list, ) @@ -936,6 +1025,9 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None: self.retrieve = async_to_streamed_response_wrapper( browsers.retrieve, ) + self.update = async_to_streamed_response_wrapper( + browsers.update, + ) self.list = async_to_streamed_response_wrapper( browsers.list, ) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 0665e536..d0375785 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -26,6 +26,7 @@ from .browser_create_params import BrowserCreateParams as BrowserCreateParams from .browser_delete_params import BrowserDeleteParams as BrowserDeleteParams from .browser_list_response import BrowserListResponse as BrowserListResponse +from .browser_update_params import BrowserUpdateParams as BrowserUpdateParams from .profile_create_params import ProfileCreateParams as ProfileCreateParams from .profile_list_response import ProfileListResponse as ProfileListResponse from .proxy_create_response import ProxyCreateResponse as ProxyCreateResponse @@ -35,6 +36,7 @@ from .invocation_list_params import InvocationListParams as InvocationListParams from .invocation_state_event import InvocationStateEvent as InvocationStateEvent from .browser_create_response import BrowserCreateResponse as BrowserCreateResponse +from .browser_update_response import BrowserUpdateResponse as BrowserUpdateResponse from .extension_list_response import ExtensionListResponse as ExtensionListResponse from .extension_upload_params import ExtensionUploadParams as ExtensionUploadParams from .proxy_retrieve_response import ProxyRetrieveResponse as ProxyRetrieveResponse diff --git a/src/kernel/types/browser_update_params.py b/src/kernel/types/browser_update_params.py new file mode 100644 index 00000000..72e02870 --- /dev/null +++ b/src/kernel/types/browser_update_params.py @@ -0,0 +1,16 @@ +# 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 TypedDict + +__all__ = ["BrowserUpdateParams"] + + +class BrowserUpdateParams(TypedDict, total=False): + proxy_id: Optional[str] + """ID of the proxy to use. + + Omit to leave unchanged, set to empty string to remove proxy. + """ diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py new file mode 100644 index 00000000..d5cb3150 --- /dev/null +++ b/src/kernel/types/browser_update_response.py @@ -0,0 +1,64 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime + +from .profile import Profile +from .._models import BaseModel +from .browser_persistence import BrowserPersistence +from .shared.browser_viewport import BrowserViewport + +__all__ = ["BrowserUpdateResponse"] + + +class BrowserUpdateResponse(BaseModel): + cdp_ws_url: str + """Websocket URL for Chrome DevTools Protocol connections to the browser session""" + + created_at: datetime + """When the browser session was created.""" + + headless: bool + """Whether the browser session is running in headless mode.""" + + session_id: str + """Unique identifier for the browser session""" + + stealth: bool + """Whether the browser session is running in stealth mode.""" + + timeout_seconds: int + """The number of seconds of inactivity before the browser session is terminated.""" + + browser_live_view_url: Optional[str] = None + """Remote URL for live viewing the browser session. + + Only available for non-headless browsers. + """ + + deleted_at: Optional[datetime] = None + """When the browser session was soft-deleted. Only present for deleted sessions.""" + + kiosk_mode: Optional[bool] = None + """Whether the browser session is running in kiosk mode.""" + + persistence: Optional[BrowserPersistence] = None + """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead.""" + + profile: Optional[Profile] = None + """Browser profile metadata.""" + + proxy_id: Optional[str] = None + """ID of the proxy associated with this browser session, if any.""" + + viewport: Optional[BrowserViewport] = None + """Initial browser window size in pixels with optional refresh rate. + + If omitted, image defaults apply (1920x1080@25). Only specific viewport + configurations are supported. The server will reject unsupported combinations. + Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will + be automatically determined from the width and height if they match a supported + configuration exactly. Note: Higher resolutions may affect the responsiveness of + live view browser + """ diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index a7666565..5eec9def 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -12,6 +12,7 @@ from kernel.types import ( BrowserListResponse, BrowserCreateResponse, + BrowserUpdateResponse, BrowserRetrieveResponse, ) from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination @@ -124,6 +125,57 @@ def test_path_params_retrieve(self, client: Kernel) -> None: "", ) + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_update(self, client: Kernel) -> None: + browser = client.browsers.update( + id="htzv5orfit78e1m2biiifpbv", + ) + assert_matches_type(BrowserUpdateResponse, browser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_update_with_all_params(self, client: Kernel) -> None: + browser = client.browsers.update( + id="htzv5orfit78e1m2biiifpbv", + proxy_id="proxy_id", + ) + assert_matches_type(BrowserUpdateResponse, browser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_update(self, client: Kernel) -> None: + response = client.browsers.with_raw_response.update( + id="htzv5orfit78e1m2biiifpbv", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser = response.parse() + assert_matches_type(BrowserUpdateResponse, browser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_update(self, client: Kernel) -> None: + with client.browsers.with_streaming_response.update( + id="htzv5orfit78e1m2biiifpbv", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser = response.parse() + assert_matches_type(BrowserUpdateResponse, browser, 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: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.with_raw_response.update( + id="", + ) + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_list(self, client: Kernel) -> None: @@ -414,6 +466,57 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: "", ) + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_update(self, async_client: AsyncKernel) -> None: + browser = await async_client.browsers.update( + id="htzv5orfit78e1m2biiifpbv", + ) + assert_matches_type(BrowserUpdateResponse, browser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> None: + browser = await async_client.browsers.update( + id="htzv5orfit78e1m2biiifpbv", + proxy_id="proxy_id", + ) + assert_matches_type(BrowserUpdateResponse, browser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_update(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.with_raw_response.update( + id="htzv5orfit78e1m2biiifpbv", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser = await response.parse() + assert_matches_type(BrowserUpdateResponse, browser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_update(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.with_streaming_response.update( + id="htzv5orfit78e1m2biiifpbv", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser = await response.parse() + assert_matches_type(BrowserUpdateResponse, browser, 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: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.with_raw_response.update( + id="", + ) + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_list(self, async_client: AsyncKernel) -> None: From 815c46da97e82b78c5605ff8a089709c72787407 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 17:12:35 +0000 Subject: [PATCH 258/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d2d60a3d..a36746b8 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.24.0" + ".": "0.25.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 22aadee5..011e9f05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.24.0" +version = "0.25.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 17d46b5d..d65bad29 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.24.0" # x-release-please-version +__version__ = "0.25.0" # x-release-please-version From a445d9dcef7d911c5e89f33122476fd771790335 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 20:10:27 +0000 Subject: [PATCH 259/448] feat: Auth agents auth check URL --- .stats.yml | 4 ++-- src/kernel/types/agents/auth_agent.py | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 330b531e..b78a34d4 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 91 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-4f5307643555b7917e8681b1966ae0b99f770cf59805e2f917ec7528edf11ba8.yml -openapi_spec_hash: 873a9aa3a88b6cec1ad94f848eeb1c45 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-fc2c80b398a8dd511010ae7cda5e21c353e388ee130aa288974b47af4208b5b8.yml +openapi_spec_hash: 5e06586dbbb9fce12b907f4e32497006 config_hash: 20337f7888852c165d099faa7589c90a diff --git a/src/kernel/types/agents/auth_agent.py b/src/kernel/types/agents/auth_agent.py index 33fc46b2..7b23b689 100644 --- a/src/kernel/types/agents/auth_agent.py +++ b/src/kernel/types/agents/auth_agent.py @@ -52,3 +52,9 @@ class AuthAgent(BaseModel): last_auth_check_at: Optional[datetime] = None """When the last authentication check was performed""" + + post_login_url: Optional[str] = None + """URL where the browser landed after successful login. + + Query parameters and fragments are stripped for privacy. + """ From a86c1d20d4a88229c96782b617d365baff2d25c6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 17 Jan 2026 04:34:02 +0000 Subject: [PATCH 260/448] chore(internal): update `actions/checkout` version --- .github/workflows/ci.yml | 6 +++--- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/release-doctor.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 795bdad6..bc3b2d2d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/kernel-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | @@ -44,7 +44,7 @@ jobs: id-token: write runs-on: ${{ github.repository == 'stainless-sdks/kernel-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | @@ -81,7 +81,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/kernel-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 994e6250..2e95b5a9 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index ba1be2c7..48941b69 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'kernel/kernel-python-sdk' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Check release environment run: | From 947b42101dde32f4466aa1391728509403bd9c59 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 17 Jan 2026 19:28:18 +0000 Subject: [PATCH 261/448] fix(stainless): use @onkernel/sdk package name for TypeScript SDK --- .stats.yml | 2 +- README.md | 2 +- src/kernel/_client.py | 2 +- tests/test_client.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.stats.yml b/.stats.yml index b78a34d4..2857ba88 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 91 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-fc2c80b398a8dd511010ae7cda5e21c353e388ee130aa288974b47af4208b5b8.yml openapi_spec_hash: 5e06586dbbb9fce12b907f4e32497006 -config_hash: 20337f7888852c165d099faa7589c90a +config_hash: cc7fdd701d995d4b3456d77041c604cf diff --git a/README.md b/README.md index 2826e5cd..2ac7afba 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ It is generated with [Stainless](https://www.stainless.com/). ## Documentation -The REST API documentation can be found on [docs.kernel.com](https://docs.kernel.com). The full API of this library can be found in [api.md](api.md). +The REST API documentation can be found on [kernel.sh](https://kernel.sh/docs). The full API of this library can be found in [api.md](api.md). ## Installation diff --git a/src/kernel/_client.py b/src/kernel/_client.py index 79410d08..166ecdb2 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -67,7 +67,7 @@ ] ENVIRONMENTS: Dict[str, str] = { - "production": "https://api.kernel.com/", + "production": "https://api.onkernel.com/", "development": "https://localhost:3001/", } diff --git a/tests/test_client.py b/tests/test_client.py index e52baa48..4d3fb09a 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -695,7 +695,7 @@ def test_base_url_env(self) -> None: Kernel(api_key=api_key, _strict_response_validation=True, environment="production") client = Kernel(base_url=None, api_key=api_key, _strict_response_validation=True, environment="production") - assert str(client.base_url).startswith("https://api.kernel.com/") + assert str(client.base_url).startswith("https://api.onkernel.com/") client.close() @@ -1596,7 +1596,7 @@ async def test_base_url_env(self) -> None: client = AsyncKernel( base_url=None, api_key=api_key, _strict_response_validation=True, environment="production" ) - assert str(client.base_url).startswith("https://api.kernel.com/") + assert str(client.base_url).startswith("https://api.onkernel.com/") await client.close() From 5249762a46eb72fefca044a69824bab9d256ea97 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 17 Jan 2026 20:31:43 +0000 Subject: [PATCH 262/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index a36746b8..caf5ca3f 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.25.0" + ".": "0.26.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 011e9f05..cad7dd22 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.25.0" +version = "0.26.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index d65bad29..0c46e78a 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.25.0" # x-release-please-version +__version__ = "0.26.0" # x-release-please-version From b6b02e3ab21fb1569274521db65971b62581b7ab Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 17 Jan 2026 21:34:40 +0000 Subject: [PATCH 263/448] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 2857ba88..8e09c0a1 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 91 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-fc2c80b398a8dd511010ae7cda5e21c353e388ee130aa288974b47af4208b5b8.yml -openapi_spec_hash: 5e06586dbbb9fce12b907f4e32497006 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-1e490dbef30dfa53ccba72524fcba4079f244f2530a4f770c00f8fee707eaa72.yml +openapi_spec_hash: 1fd15429610959f19aed6d3cb170ab9e config_hash: cc7fdd701d995d4b3456d77041c604cf From 7c0284773548ca85b22407902b5031cdf1ebf84a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 18 Jan 2026 22:56:59 +0000 Subject: [PATCH 264/448] feat(dashboard): add browser replays support for past browsers --- .stats.yml | 4 +- api.md | 2 +- src/kernel/resources/browsers/browsers.py | 50 ++++++++++++++++----- src/kernel/types/__init__.py | 1 + src/kernel/types/browser_list_params.py | 12 ++++- src/kernel/types/browser_retrieve_params.py | 12 +++++ tests/api_resources/test_browsers.py | 36 +++++++++++---- 7 files changed, 94 insertions(+), 23 deletions(-) create mode 100644 src/kernel/types/browser_retrieve_params.py diff --git a/.stats.yml b/.stats.yml index 8e09c0a1..37fa5b76 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 91 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-1e490dbef30dfa53ccba72524fcba4079f244f2530a4f770c00f8fee707eaa72.yml -openapi_spec_hash: 1fd15429610959f19aed6d3cb170ab9e +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-68729f2ff40476377ead9019c18ea140fc4efbc2e68d7c4fc323bd61ae81f768.yml +openapi_spec_hash: 9eec61481f9059b5fedc13abc3e39338 config_hash: cc7fdd701d995d4b3456d77041c604cf diff --git a/api.md b/api.md index 51f8cdb4..a665bc2b 100644 --- a/api.md +++ b/api.md @@ -89,7 +89,7 @@ from kernel.types import ( Methods: - client.browsers.create(\*\*params) -> BrowserCreateResponse -- client.browsers.retrieve(id) -> BrowserRetrieveResponse +- client.browsers.retrieve(id, \*\*params) -> BrowserRetrieveResponse - client.browsers.update(id, \*\*params) -> BrowserUpdateResponse - client.browsers.list(\*\*params) -> SyncOffsetPagination[BrowserListResponse] - client.browsers.delete(\*\*params) -> None diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index ab57854c..d835f7ac 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -4,6 +4,7 @@ import typing_extensions from typing import Mapping, Iterable, Optional, cast +from typing_extensions import Literal import httpx @@ -28,6 +29,7 @@ browser_create_params, browser_delete_params, browser_update_params, + browser_retrieve_params, browser_load_extensions_params, ) from .process import ( @@ -226,6 +228,7 @@ def retrieve( self, id: str, *, + include_deleted: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -237,6 +240,8 @@ def retrieve( Get information about a browser session. Args: + include_deleted: When true, includes soft-deleted browser sessions in the lookup. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -250,7 +255,13 @@ def retrieve( return self._get( f"/browsers/{id}", options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + {"include_deleted": include_deleted}, browser_retrieve_params.BrowserRetrieveParams + ), ), cast_to=BrowserRetrieveResponse, ) @@ -300,6 +311,7 @@ def list( include_deleted: bool | Omit = omit, limit: int | Omit = omit, offset: int | Omit = omit, + status: Literal["active", "deleted", "all"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -309,17 +321,20 @@ def list( ) -> SyncOffsetPagination[BrowserListResponse]: """List all browser sessions with pagination support. - Use include_deleted=true to - include soft-deleted sessions in the results. + Use status parameter to + filter by session state. Args: - include_deleted: When true, includes soft-deleted browser sessions in the results alongside - active sessions. + include_deleted: Deprecated: Use status=all instead. When true, includes soft-deleted browser + sessions in the results alongside active sessions. limit: Maximum number of results to return. Defaults to 20, maximum 100. offset: Number of results to skip. Defaults to 0. + status: Filter sessions by status. "active" returns only active sessions (default), + "deleted" returns only soft-deleted sessions, "all" returns both. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -341,6 +356,7 @@ def list( "include_deleted": include_deleted, "limit": limit, "offset": offset, + "status": status, }, browser_list_params.BrowserListParams, ), @@ -610,6 +626,7 @@ async def retrieve( self, id: str, *, + include_deleted: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -621,6 +638,8 @@ async def retrieve( Get information about a browser session. Args: + include_deleted: When true, includes soft-deleted browser sessions in the lookup. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -634,7 +653,13 @@ async def retrieve( return await self._get( f"/browsers/{id}", options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + {"include_deleted": include_deleted}, browser_retrieve_params.BrowserRetrieveParams + ), ), cast_to=BrowserRetrieveResponse, ) @@ -684,6 +709,7 @@ def list( include_deleted: bool | Omit = omit, limit: int | Omit = omit, offset: int | Omit = omit, + status: Literal["active", "deleted", "all"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -693,17 +719,20 @@ def list( ) -> AsyncPaginator[BrowserListResponse, AsyncOffsetPagination[BrowserListResponse]]: """List all browser sessions with pagination support. - Use include_deleted=true to - include soft-deleted sessions in the results. + Use status parameter to + filter by session state. Args: - include_deleted: When true, includes soft-deleted browser sessions in the results alongside - active sessions. + include_deleted: Deprecated: Use status=all instead. When true, includes soft-deleted browser + sessions in the results alongside active sessions. limit: Maximum number of results to return. Defaults to 20, maximum 100. offset: Number of results to skip. Defaults to 0. + status: Filter sessions by status. "active" returns only active sessions (default), + "deleted" returns only soft-deleted sessions, "all" returns both. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -725,6 +754,7 @@ def list( "include_deleted": include_deleted, "limit": limit, "offset": offset, + "status": status, }, browser_list_params.BrowserListParams, ), diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index d0375785..016e17c9 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -36,6 +36,7 @@ from .invocation_list_params import InvocationListParams as InvocationListParams from .invocation_state_event import InvocationStateEvent as InvocationStateEvent from .browser_create_response import BrowserCreateResponse as BrowserCreateResponse +from .browser_retrieve_params import BrowserRetrieveParams as BrowserRetrieveParams from .browser_update_response import BrowserUpdateResponse as BrowserUpdateResponse from .extension_list_response import ExtensionListResponse as ExtensionListResponse from .extension_upload_params import ExtensionUploadParams as ExtensionUploadParams diff --git a/src/kernel/types/browser_list_params.py b/src/kernel/types/browser_list_params.py index 20837bef..02aa97a6 100644 --- a/src/kernel/types/browser_list_params.py +++ b/src/kernel/types/browser_list_params.py @@ -2,14 +2,15 @@ from __future__ import annotations -from typing_extensions import TypedDict +from typing_extensions import Literal, TypedDict __all__ = ["BrowserListParams"] class BrowserListParams(TypedDict, total=False): include_deleted: bool - """ + """Deprecated: Use status=all instead. + When true, includes soft-deleted browser sessions in the results alongside active sessions. """ @@ -19,3 +20,10 @@ class BrowserListParams(TypedDict, total=False): offset: int """Number of results to skip. Defaults to 0.""" + + status: Literal["active", "deleted", "all"] + """Filter sessions by status. + + "active" returns only active sessions (default), "deleted" returns only + soft-deleted sessions, "all" returns both. + """ diff --git a/src/kernel/types/browser_retrieve_params.py b/src/kernel/types/browser_retrieve_params.py new file mode 100644 index 00000000..ec5e8aa1 --- /dev/null +++ b/src/kernel/types/browser_retrieve_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["BrowserRetrieveParams"] + + +class BrowserRetrieveParams(TypedDict, total=False): + include_deleted: bool + """When true, includes soft-deleted browser sessions in the lookup.""" diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index 5eec9def..139cf143 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -87,7 +87,16 @@ def test_streaming_response_create(self, client: Kernel) -> None: @parametrize def test_method_retrieve(self, client: Kernel) -> None: browser = client.browsers.retrieve( - "htzv5orfit78e1m2biiifpbv", + id="htzv5orfit78e1m2biiifpbv", + ) + assert_matches_type(BrowserRetrieveResponse, browser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_retrieve_with_all_params(self, client: Kernel) -> None: + browser = client.browsers.retrieve( + id="htzv5orfit78e1m2biiifpbv", + include_deleted=True, ) assert_matches_type(BrowserRetrieveResponse, browser, path=["response"]) @@ -95,7 +104,7 @@ def test_method_retrieve(self, client: Kernel) -> None: @parametrize def test_raw_response_retrieve(self, client: Kernel) -> None: response = client.browsers.with_raw_response.retrieve( - "htzv5orfit78e1m2biiifpbv", + id="htzv5orfit78e1m2biiifpbv", ) assert response.is_closed is True @@ -107,7 +116,7 @@ def test_raw_response_retrieve(self, client: Kernel) -> None: @parametrize def test_streaming_response_retrieve(self, client: Kernel) -> None: with client.browsers.with_streaming_response.retrieve( - "htzv5orfit78e1m2biiifpbv", + id="htzv5orfit78e1m2biiifpbv", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -122,7 +131,7 @@ def test_streaming_response_retrieve(self, client: Kernel) -> None: def test_path_params_retrieve(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): client.browsers.with_raw_response.retrieve( - "", + id="", ) @pytest.mark.skip(reason="Prism tests are disabled") @@ -189,6 +198,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None: include_deleted=True, limit=1, offset=0, + status="active", ) assert_matches_type(SyncOffsetPagination[BrowserListResponse], browser, path=["response"]) @@ -428,7 +438,16 @@ async def test_streaming_response_create(self, async_client: AsyncKernel) -> Non @parametrize async def test_method_retrieve(self, async_client: AsyncKernel) -> None: browser = await async_client.browsers.retrieve( - "htzv5orfit78e1m2biiifpbv", + id="htzv5orfit78e1m2biiifpbv", + ) + assert_matches_type(BrowserRetrieveResponse, browser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_retrieve_with_all_params(self, async_client: AsyncKernel) -> None: + browser = await async_client.browsers.retrieve( + id="htzv5orfit78e1m2biiifpbv", + include_deleted=True, ) assert_matches_type(BrowserRetrieveResponse, browser, path=["response"]) @@ -436,7 +455,7 @@ async def test_method_retrieve(self, async_client: AsyncKernel) -> None: @parametrize async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.with_raw_response.retrieve( - "htzv5orfit78e1m2biiifpbv", + id="htzv5orfit78e1m2biiifpbv", ) assert response.is_closed is True @@ -448,7 +467,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: async with async_client.browsers.with_streaming_response.retrieve( - "htzv5orfit78e1m2biiifpbv", + id="htzv5orfit78e1m2biiifpbv", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -463,7 +482,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> N async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): await async_client.browsers.with_raw_response.retrieve( - "", + id="", ) @pytest.mark.skip(reason="Prism tests are disabled") @@ -530,6 +549,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N include_deleted=True, limit=1, offset=0, + status="active", ) assert_matches_type(AsyncOffsetPagination[BrowserListResponse], browser, path=["response"]) From e1a8a81488c312fb342bd33e1aa81661ef1c684e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 19 Jan 2026 18:47:35 +0000 Subject: [PATCH 265/448] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 37fa5b76..9ec6fcc6 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 91 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-68729f2ff40476377ead9019c18ea140fc4efbc2e68d7c4fc323bd61ae81f768.yml -openapi_spec_hash: 9eec61481f9059b5fedc13abc3e39338 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-59d2925a3cb93809cc762a3ac350691b365898e284f2c66a5999b9a6a37a35e5.yml +openapi_spec_hash: dfcb0a49e657426d0c0f44cfa3e89430 config_hash: cc7fdd701d995d4b3456d77041c604cf From 84fdef860760ce162a76106657e8208927e7ff19 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 01:00:01 +0000 Subject: [PATCH 266/448] feat: Update browser pool org limits --- .stats.yml | 4 ++-- src/kernel/resources/browser_pools.py | 16 ++++++++++++---- src/kernel/types/browser_pool.py | 6 +++++- src/kernel/types/browser_pool_create_params.py | 6 +++++- src/kernel/types/browser_pool_update_params.py | 6 +++++- 5 files changed, 29 insertions(+), 9 deletions(-) diff --git a/.stats.yml b/.stats.yml index 9ec6fcc6..996826a1 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 91 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-59d2925a3cb93809cc762a3ac350691b365898e284f2c66a5999b9a6a37a35e5.yml -openapi_spec_hash: dfcb0a49e657426d0c0f44cfa3e89430 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-97558c7b5f2714e629041ff892cdabef76c4ab214b5f908ba8b36d507eac5260.yml +openapi_spec_hash: 3464d532154863ca17b82082451b9faf config_hash: cc7fdd701d995d4b3456d77041c604cf diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py index 5a4bf61b..884b0e16 100644 --- a/src/kernel/resources/browser_pools.py +++ b/src/kernel/resources/browser_pools.py @@ -79,7 +79,9 @@ def create( Create a new browser pool with the specified configuration and size. Args: - size: Number of browsers to create in the pool + size: Number of browsers to maintain in the pool. The maximum size is determined by + your organization's pooled sessions limit (the sum of all pool sizes cannot + exceed your limit). extensions: List of browser extensions to load into the session. Provide each by id or name. @@ -206,7 +208,9 @@ def update( Updates the configuration used to create browsers in the pool. Args: - size: Number of browsers to create in the pool + size: Number of browsers to maintain in the pool. The maximum size is determined by + your organization's pooled sessions limit (the sum of all pool sizes cannot + exceed your limit). discard_all_idle: Whether to discard all idle browsers and rebuild the pool immediately. Defaults to false. @@ -513,7 +517,9 @@ async def create( Create a new browser pool with the specified configuration and size. Args: - size: Number of browsers to create in the pool + size: Number of browsers to maintain in the pool. The maximum size is determined by + your organization's pooled sessions limit (the sum of all pool sizes cannot + exceed your limit). extensions: List of browser extensions to load into the session. Provide each by id or name. @@ -640,7 +646,9 @@ async def update( Updates the configuration used to create browsers in the pool. Args: - size: Number of browsers to create in the pool + size: Number of browsers to maintain in the pool. The maximum size is determined by + your organization's pooled sessions limit (the sum of all pool sizes cannot + exceed your limit). discard_all_idle: Whether to discard all idle browsers and rebuild the pool immediately. Defaults to false. diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py index 1694313c..b9142f7c 100644 --- a/src/kernel/types/browser_pool.py +++ b/src/kernel/types/browser_pool.py @@ -15,7 +15,11 @@ class BrowserPoolConfig(BaseModel): """Configuration used to create all browsers in this pool""" size: int - """Number of browsers to create in the pool""" + """Number of browsers to maintain in the pool. + + The maximum size is determined by your organization's pooled sessions limit (the + sum of all pool sizes cannot exceed your limit). + """ extensions: Optional[List[BrowserExtension]] = None """List of browser extensions to load into the session. diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py index 6c8e815e..afa9e6e5 100644 --- a/src/kernel/types/browser_pool_create_params.py +++ b/src/kernel/types/browser_pool_create_params.py @@ -14,7 +14,11 @@ class BrowserPoolCreateParams(TypedDict, total=False): size: Required[int] - """Number of browsers to create in the pool""" + """Number of browsers to maintain in the pool. + + The maximum size is determined by your organization's pooled sessions limit (the + sum of all pool sizes cannot exceed your limit). + """ extensions: Iterable[BrowserExtension] """List of browser extensions to load into the session. diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py index 2cd3be71..ac23916a 100644 --- a/src/kernel/types/browser_pool_update_params.py +++ b/src/kernel/types/browser_pool_update_params.py @@ -14,7 +14,11 @@ class BrowserPoolUpdateParams(TypedDict, total=False): size: Required[int] - """Number of browsers to create in the pool""" + """Number of browsers to maintain in the pool. + + The maximum size is determined by your organization's pooled sessions limit (the + sum of all pool sizes cannot exceed your limit). + """ discard_all_idle: bool """Whether to discard all idle browsers and rebuild the pool immediately. From d3f0fd0b6012d2850db70bc6f474888476fb4b00 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 03:29:37 +0000 Subject: [PATCH 267/448] =?UTF-8?q?refactor(agentauth):=20enhance=20discov?= =?UTF-8?q?er=20and=20submit=20modules=20with=20improve=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .stats.yml | 4 ++-- src/kernel/types/agents/agent_auth_invocation_response.py | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 996826a1..480b78b8 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 91 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-97558c7b5f2714e629041ff892cdabef76c4ab214b5f908ba8b36d507eac5260.yml -openapi_spec_hash: 3464d532154863ca17b82082451b9faf +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-714affeb2859c03a71d35708b6704b1750a1712738a130f3363ae67b20d751d9.yml +openapi_spec_hash: 9d2b9358f0f640ecd1eacd15b70dd361 config_hash: cc7fdd701d995d4b3456d77041c604cf diff --git a/src/kernel/types/agents/agent_auth_invocation_response.py b/src/kernel/types/agents/agent_auth_invocation_response.py index 2731290e..98235826 100644 --- a/src/kernel/types/agents/agent_auth_invocation_response.py +++ b/src/kernel/types/agents/agent_auth_invocation_response.py @@ -91,6 +91,9 @@ class AgentAuthInvocationResponse(BaseModel): pending_sso_buttons: Optional[List[PendingSSOButton]] = None """SSO buttons available on the page (present when step=awaiting_input)""" + sso_provider: Optional[str] = None + """SSO provider being used for authentication (e.g., google, github, microsoft)""" + submitted_fields: Optional[List[str]] = None """ Names of fields that have been submitted (present when step=submitting or later) From 9fab5d588ae184b686de4b45ba0dc0b040da91c8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 04:39:07 +0000 Subject: [PATCH 268/448] feat(agent-auth): add 1Password integration for credential providers --- .stats.yml | 8 +- api.md | 23 + src/kernel/_client.py | 38 ++ src/kernel/resources/__init__.py | 14 + src/kernel/resources/credential_providers.py | 605 ++++++++++++++++++ src/kernel/types/__init__.py | 5 + src/kernel/types/credential_provider.py | 32 + .../credential_provider_create_params.py | 18 + .../credential_provider_list_response.py | 10 + .../types/credential_provider_test_result.py | 28 + .../credential_provider_update_params.py | 21 + .../test_credential_providers.py | 538 ++++++++++++++++ 12 files changed, 1336 insertions(+), 4 deletions(-) create mode 100644 src/kernel/resources/credential_providers.py create mode 100644 src/kernel/types/credential_provider.py create mode 100644 src/kernel/types/credential_provider_create_params.py create mode 100644 src/kernel/types/credential_provider_list_response.py create mode 100644 src/kernel/types/credential_provider_test_result.py create mode 100644 src/kernel/types/credential_provider_update_params.py create mode 100644 tests/api_resources/test_credential_providers.py diff --git a/.stats.yml b/.stats.yml index 480b78b8..50043a40 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 91 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-714affeb2859c03a71d35708b6704b1750a1712738a130f3363ae67b20d751d9.yml -openapi_spec_hash: 9d2b9358f0f640ecd1eacd15b70dd361 -config_hash: cc7fdd701d995d4b3456d77041c604cf +configured_endpoints: 97 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-7427d4bcaba5cad07910da7a222bdd2650b5280e6b889132ed38d230adafb8a5.yml +openapi_spec_hash: e8e3dc1ae54666d544d1fc848b25e7cf +config_hash: b470456b217bb9502f5212311d395a6f diff --git a/api.md b/api.md index a665bc2b..e951a78a 100644 --- a/api.md +++ b/api.md @@ -344,3 +344,26 @@ Methods: - client.credentials.list(\*\*params) -> SyncOffsetPagination[Credential] - client.credentials.delete(id_or_name) -> None - client.credentials.totp_code(id_or_name) -> CredentialTotpCodeResponse + +# CredentialProviders + +Types: + +```python +from kernel.types import ( + CreateCredentialProviderRequest, + CredentialProvider, + CredentialProviderTestResult, + UpdateCredentialProviderRequest, + CredentialProviderListResponse, +) +``` + +Methods: + +- client.credential_providers.create(\*\*params) -> CredentialProvider +- client.credential_providers.retrieve(id) -> CredentialProvider +- client.credential_providers.update(id, \*\*params) -> CredentialProvider +- client.credential_providers.list() -> CredentialProviderListResponse +- client.credential_providers.delete(id) -> None +- client.credential_providers.test(id) -> CredentialProviderTestResult diff --git a/src/kernel/_client.py b/src/kernel/_client.py index 166ecdb2..daf57998 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -42,6 +42,7 @@ deployments, invocations, browser_pools, + credential_providers, ) from .resources.apps import AppsResource, AsyncAppsResource from .resources.proxies import ProxiesResource, AsyncProxiesResource @@ -53,6 +54,7 @@ from .resources.agents.agents import AgentsResource, AsyncAgentsResource from .resources.browser_pools import BrowserPoolsResource, AsyncBrowserPoolsResource from .resources.browsers.browsers import BrowsersResource, AsyncBrowsersResource + from .resources.credential_providers import CredentialProvidersResource, AsyncCredentialProvidersResource __all__ = [ "ENVIRONMENTS", @@ -211,6 +213,12 @@ def credentials(self) -> CredentialsResource: return CredentialsResource(self) + @cached_property + def credential_providers(self) -> CredentialProvidersResource: + from .resources.credential_providers import CredentialProvidersResource + + return CredentialProvidersResource(self) + @cached_property def with_raw_response(self) -> KernelWithRawResponse: return KernelWithRawResponse(self) @@ -465,6 +473,12 @@ def credentials(self) -> AsyncCredentialsResource: return AsyncCredentialsResource(self) + @cached_property + def credential_providers(self) -> AsyncCredentialProvidersResource: + from .resources.credential_providers import AsyncCredentialProvidersResource + + return AsyncCredentialProvidersResource(self) + @cached_property def with_raw_response(self) -> AsyncKernelWithRawResponse: return AsyncKernelWithRawResponse(self) @@ -646,6 +660,12 @@ def credentials(self) -> credentials.CredentialsResourceWithRawResponse: return CredentialsResourceWithRawResponse(self._client.credentials) + @cached_property + def credential_providers(self) -> credential_providers.CredentialProvidersResourceWithRawResponse: + from .resources.credential_providers import CredentialProvidersResourceWithRawResponse + + return CredentialProvidersResourceWithRawResponse(self._client.credential_providers) + class AsyncKernelWithRawResponse: _client: AsyncKernel @@ -713,6 +733,12 @@ def credentials(self) -> credentials.AsyncCredentialsResourceWithRawResponse: return AsyncCredentialsResourceWithRawResponse(self._client.credentials) + @cached_property + def credential_providers(self) -> credential_providers.AsyncCredentialProvidersResourceWithRawResponse: + from .resources.credential_providers import AsyncCredentialProvidersResourceWithRawResponse + + return AsyncCredentialProvidersResourceWithRawResponse(self._client.credential_providers) + class KernelWithStreamedResponse: _client: Kernel @@ -780,6 +806,12 @@ def credentials(self) -> credentials.CredentialsResourceWithStreamingResponse: return CredentialsResourceWithStreamingResponse(self._client.credentials) + @cached_property + def credential_providers(self) -> credential_providers.CredentialProvidersResourceWithStreamingResponse: + from .resources.credential_providers import CredentialProvidersResourceWithStreamingResponse + + return CredentialProvidersResourceWithStreamingResponse(self._client.credential_providers) + class AsyncKernelWithStreamedResponse: _client: AsyncKernel @@ -847,6 +879,12 @@ def credentials(self) -> credentials.AsyncCredentialsResourceWithStreamingRespon return AsyncCredentialsResourceWithStreamingResponse(self._client.credentials) + @cached_property + def credential_providers(self) -> credential_providers.AsyncCredentialProvidersResourceWithStreamingResponse: + from .resources.credential_providers import AsyncCredentialProvidersResourceWithStreamingResponse + + return AsyncCredentialProvidersResourceWithStreamingResponse(self._client.credential_providers) + Client = Kernel diff --git a/src/kernel/resources/__init__.py b/src/kernel/resources/__init__.py index e6e81036..50db63bb 100644 --- a/src/kernel/resources/__init__.py +++ b/src/kernel/resources/__init__.py @@ -80,6 +80,14 @@ BrowserPoolsResourceWithStreamingResponse, AsyncBrowserPoolsResourceWithStreamingResponse, ) +from .credential_providers import ( + CredentialProvidersResource, + AsyncCredentialProvidersResource, + CredentialProvidersResourceWithRawResponse, + AsyncCredentialProvidersResourceWithRawResponse, + CredentialProvidersResourceWithStreamingResponse, + AsyncCredentialProvidersResourceWithStreamingResponse, +) __all__ = [ "DeploymentsResource", @@ -142,4 +150,10 @@ "AsyncCredentialsResourceWithRawResponse", "CredentialsResourceWithStreamingResponse", "AsyncCredentialsResourceWithStreamingResponse", + "CredentialProvidersResource", + "AsyncCredentialProvidersResource", + "CredentialProvidersResourceWithRawResponse", + "AsyncCredentialProvidersResourceWithRawResponse", + "CredentialProvidersResourceWithStreamingResponse", + "AsyncCredentialProvidersResourceWithStreamingResponse", ] diff --git a/src/kernel/resources/credential_providers.py b/src/kernel/resources/credential_providers.py new file mode 100644 index 00000000..b1b1248c --- /dev/null +++ b/src/kernel/resources/credential_providers.py @@ -0,0 +1,605 @@ +# 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 credential_provider_create_params, credential_provider_update_params +from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given +from .._utils import maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +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.credential_provider import CredentialProvider +from ..types.credential_provider_test_result import CredentialProviderTestResult +from ..types.credential_provider_list_response import CredentialProviderListResponse + +__all__ = ["CredentialProvidersResource", "AsyncCredentialProvidersResource"] + + +class CredentialProvidersResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> CredentialProvidersResourceWithRawResponse: + """ + 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/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return CredentialProvidersResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> CredentialProvidersResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response + """ + return CredentialProvidersResourceWithStreamingResponse(self) + + def create( + self, + *, + token: str, + provider_type: Literal["onepassword"], + cache_ttl_seconds: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> CredentialProvider: + """ + Configure an external credential provider (e.g., 1Password) for automatic + credential lookup. + + Args: + token: Service account token for the provider (e.g., 1Password service account token) + + provider_type: Type of credential provider + + cache_ttl_seconds: How long to cache credential lists (default 300 seconds) + + 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( + "/org/credential-providers", + body=maybe_transform( + { + "token": token, + "provider_type": provider_type, + "cache_ttl_seconds": cache_ttl_seconds, + }, + credential_provider_create_params.CredentialProviderCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CredentialProvider, + ) + + def retrieve( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> CredentialProvider: + """ + Retrieve a credential provider by its ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/org/credential-providers/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CredentialProvider, + ) + + def update( + self, + id: str, + *, + token: str | Omit = omit, + cache_ttl_seconds: int | Omit = omit, + enabled: bool | Omit = omit, + priority: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> CredentialProvider: + """ + Update a credential provider's configuration. + + Args: + token: New service account token (to rotate credentials) + + cache_ttl_seconds: How long to cache credential lists + + enabled: Whether the provider is enabled for credential lookups + + priority: Priority order for credential lookups (lower numbers are checked first) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._patch( + f"/org/credential-providers/{id}", + body=maybe_transform( + { + "token": token, + "cache_ttl_seconds": cache_ttl_seconds, + "enabled": enabled, + "priority": priority, + }, + credential_provider_update_params.CredentialProviderUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CredentialProvider, + ) + + def list( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> CredentialProviderListResponse: + """List external credential providers configured for the organization.""" + return self._get( + "/org/credential-providers", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CredentialProviderListResponse, + ) + + def delete( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + Delete a credential provider by its ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + f"/org/credential-providers/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + def test( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> CredentialProviderTestResult: + """ + Validate the credential provider's token and list accessible vaults. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/org/credential-providers/{id}/test", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CredentialProviderTestResult, + ) + + +class AsyncCredentialProvidersResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncCredentialProvidersResourceWithRawResponse: + """ + 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/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncCredentialProvidersResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncCredentialProvidersResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response + """ + return AsyncCredentialProvidersResourceWithStreamingResponse(self) + + async def create( + self, + *, + token: str, + provider_type: Literal["onepassword"], + cache_ttl_seconds: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> CredentialProvider: + """ + Configure an external credential provider (e.g., 1Password) for automatic + credential lookup. + + Args: + token: Service account token for the provider (e.g., 1Password service account token) + + provider_type: Type of credential provider + + cache_ttl_seconds: How long to cache credential lists (default 300 seconds) + + 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( + "/org/credential-providers", + body=await async_maybe_transform( + { + "token": token, + "provider_type": provider_type, + "cache_ttl_seconds": cache_ttl_seconds, + }, + credential_provider_create_params.CredentialProviderCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CredentialProvider, + ) + + async def retrieve( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> CredentialProvider: + """ + Retrieve a credential provider by its ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/org/credential-providers/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CredentialProvider, + ) + + async def update( + self, + id: str, + *, + token: str | Omit = omit, + cache_ttl_seconds: int | Omit = omit, + enabled: bool | Omit = omit, + priority: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> CredentialProvider: + """ + Update a credential provider's configuration. + + Args: + token: New service account token (to rotate credentials) + + cache_ttl_seconds: How long to cache credential lists + + enabled: Whether the provider is enabled for credential lookups + + priority: Priority order for credential lookups (lower numbers are checked first) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._patch( + f"/org/credential-providers/{id}", + body=await async_maybe_transform( + { + "token": token, + "cache_ttl_seconds": cache_ttl_seconds, + "enabled": enabled, + "priority": priority, + }, + credential_provider_update_params.CredentialProviderUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CredentialProvider, + ) + + async def list( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> CredentialProviderListResponse: + """List external credential providers configured for the organization.""" + return await self._get( + "/org/credential-providers", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CredentialProviderListResponse, + ) + + async def delete( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + Delete a credential provider by its ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + f"/org/credential-providers/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + async def test( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> CredentialProviderTestResult: + """ + Validate the credential provider's token and list accessible vaults. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/org/credential-providers/{id}/test", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CredentialProviderTestResult, + ) + + +class CredentialProvidersResourceWithRawResponse: + def __init__(self, credential_providers: CredentialProvidersResource) -> None: + self._credential_providers = credential_providers + + self.create = to_raw_response_wrapper( + credential_providers.create, + ) + self.retrieve = to_raw_response_wrapper( + credential_providers.retrieve, + ) + self.update = to_raw_response_wrapper( + credential_providers.update, + ) + self.list = to_raw_response_wrapper( + credential_providers.list, + ) + self.delete = to_raw_response_wrapper( + credential_providers.delete, + ) + self.test = to_raw_response_wrapper( + credential_providers.test, + ) + + +class AsyncCredentialProvidersResourceWithRawResponse: + def __init__(self, credential_providers: AsyncCredentialProvidersResource) -> None: + self._credential_providers = credential_providers + + self.create = async_to_raw_response_wrapper( + credential_providers.create, + ) + self.retrieve = async_to_raw_response_wrapper( + credential_providers.retrieve, + ) + self.update = async_to_raw_response_wrapper( + credential_providers.update, + ) + self.list = async_to_raw_response_wrapper( + credential_providers.list, + ) + self.delete = async_to_raw_response_wrapper( + credential_providers.delete, + ) + self.test = async_to_raw_response_wrapper( + credential_providers.test, + ) + + +class CredentialProvidersResourceWithStreamingResponse: + def __init__(self, credential_providers: CredentialProvidersResource) -> None: + self._credential_providers = credential_providers + + self.create = to_streamed_response_wrapper( + credential_providers.create, + ) + self.retrieve = to_streamed_response_wrapper( + credential_providers.retrieve, + ) + self.update = to_streamed_response_wrapper( + credential_providers.update, + ) + self.list = to_streamed_response_wrapper( + credential_providers.list, + ) + self.delete = to_streamed_response_wrapper( + credential_providers.delete, + ) + self.test = to_streamed_response_wrapper( + credential_providers.test, + ) + + +class AsyncCredentialProvidersResourceWithStreamingResponse: + def __init__(self, credential_providers: AsyncCredentialProvidersResource) -> None: + self._credential_providers = credential_providers + + self.create = async_to_streamed_response_wrapper( + credential_providers.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + credential_providers.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + credential_providers.update, + ) + self.list = async_to_streamed_response_wrapper( + credential_providers.list, + ) + self.delete = async_to_streamed_response_wrapper( + credential_providers.delete, + ) + self.test = async_to_streamed_response_wrapper( + credential_providers.test, + ) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 016e17c9..ef16ae24 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -20,6 +20,7 @@ from .app_list_response import AppListResponse as AppListResponse from .browser_list_params import BrowserListParams as BrowserListParams from .browser_persistence import BrowserPersistence as BrowserPersistence +from .credential_provider import CredentialProvider as CredentialProvider from .proxy_create_params import ProxyCreateParams as ProxyCreateParams from .proxy_list_response import ProxyListResponse as ProxyListResponse from .proxy_check_response import ProxyCheckResponse as ProxyCheckResponse @@ -69,6 +70,10 @@ from .browser_pool_acquire_response import BrowserPoolAcquireResponse as BrowserPoolAcquireResponse from .credential_totp_code_response import CredentialTotpCodeResponse as CredentialTotpCodeResponse from .browser_load_extensions_params import BrowserLoadExtensionsParams as BrowserLoadExtensionsParams +from .credential_provider_test_result import CredentialProviderTestResult as CredentialProviderTestResult +from .credential_provider_create_params import CredentialProviderCreateParams as CredentialProviderCreateParams +from .credential_provider_list_response import CredentialProviderListResponse as CredentialProviderListResponse +from .credential_provider_update_params import CredentialProviderUpdateParams as CredentialProviderUpdateParams from .extension_download_from_chrome_store_params import ( ExtensionDownloadFromChromeStoreParams as ExtensionDownloadFromChromeStoreParams, ) diff --git a/src/kernel/types/credential_provider.py b/src/kernel/types/credential_provider.py new file mode 100644 index 00000000..83a205ad --- /dev/null +++ b/src/kernel/types/credential_provider.py @@ -0,0 +1,32 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from datetime import datetime +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["CredentialProvider"] + + +class CredentialProvider(BaseModel): + """ + An external credential provider (e.g., 1Password) for automatic credential lookup + """ + + id: str + """Unique identifier for the credential provider""" + + created_at: datetime + """When the credential provider was created""" + + enabled: bool + """Whether the provider is enabled for credential lookups""" + + priority: int + """Priority order for credential lookups (lower numbers are checked first)""" + + provider_type: Literal["onepassword"] + """Type of credential provider""" + + updated_at: datetime + """When the credential provider was last updated""" diff --git a/src/kernel/types/credential_provider_create_params.py b/src/kernel/types/credential_provider_create_params.py new file mode 100644 index 00000000..ed631b39 --- /dev/null +++ b/src/kernel/types/credential_provider_create_params.py @@ -0,0 +1,18 @@ +# 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__ = ["CredentialProviderCreateParams"] + + +class CredentialProviderCreateParams(TypedDict, total=False): + token: Required[str] + """Service account token for the provider (e.g., 1Password service account token)""" + + provider_type: Required[Literal["onepassword"]] + """Type of credential provider""" + + cache_ttl_seconds: int + """How long to cache credential lists (default 300 seconds)""" diff --git a/src/kernel/types/credential_provider_list_response.py b/src/kernel/types/credential_provider_list_response.py new file mode 100644 index 00000000..59814e03 --- /dev/null +++ b/src/kernel/types/credential_provider_list_response.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List +from typing_extensions import TypeAlias + +from .credential_provider import CredentialProvider + +__all__ = ["CredentialProviderListResponse"] + +CredentialProviderListResponse: TypeAlias = List[CredentialProvider] diff --git a/src/kernel/types/credential_provider_test_result.py b/src/kernel/types/credential_provider_test_result.py new file mode 100644 index 00000000..8cc4b018 --- /dev/null +++ b/src/kernel/types/credential_provider_test_result.py @@ -0,0 +1,28 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel + +__all__ = ["CredentialProviderTestResult", "Vault"] + + +class Vault(BaseModel): + id: str + """Vault ID""" + + name: str + """Vault name""" + + +class CredentialProviderTestResult(BaseModel): + """Result of testing a credential provider connection""" + + success: bool + """Whether the connection test was successful""" + + vaults: List[Vault] + """List of vaults accessible by the service account""" + + error: Optional[str] = None + """Error message if the test failed""" diff --git a/src/kernel/types/credential_provider_update_params.py b/src/kernel/types/credential_provider_update_params.py new file mode 100644 index 00000000..ecebeab7 --- /dev/null +++ b/src/kernel/types/credential_provider_update_params.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["CredentialProviderUpdateParams"] + + +class CredentialProviderUpdateParams(TypedDict, total=False): + token: str + """New service account token (to rotate credentials)""" + + cache_ttl_seconds: int + """How long to cache credential lists""" + + enabled: bool + """Whether the provider is enabled for credential lookups""" + + priority: int + """Priority order for credential lookups (lower numbers are checked first)""" diff --git a/tests/api_resources/test_credential_providers.py b/tests/api_resources/test_credential_providers.py new file mode 100644 index 00000000..136446c0 --- /dev/null +++ b/tests/api_resources/test_credential_providers.py @@ -0,0 +1,538 @@ +# 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 kernel import Kernel, AsyncKernel +from tests.utils import assert_matches_type +from kernel.types import ( + CredentialProvider, + CredentialProviderTestResult, + CredentialProviderListResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestCredentialProviders: + 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: Kernel) -> None: + credential_provider = client.credential_providers.create( + token="ops_eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", + provider_type="onepassword", + ) + assert_matches_type(CredentialProvider, credential_provider, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_create_with_all_params(self, client: Kernel) -> None: + credential_provider = client.credential_providers.create( + token="ops_eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", + provider_type="onepassword", + cache_ttl_seconds=300, + ) + assert_matches_type(CredentialProvider, credential_provider, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_create(self, client: Kernel) -> None: + response = client.credential_providers.with_raw_response.create( + token="ops_eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", + provider_type="onepassword", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + credential_provider = response.parse() + assert_matches_type(CredentialProvider, credential_provider, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_create(self, client: Kernel) -> None: + with client.credential_providers.with_streaming_response.create( + token="ops_eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", + provider_type="onepassword", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + credential_provider = response.parse() + assert_matches_type(CredentialProvider, credential_provider, 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: Kernel) -> None: + credential_provider = client.credential_providers.retrieve( + "id", + ) + assert_matches_type(CredentialProvider, credential_provider, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: Kernel) -> None: + response = client.credential_providers.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + credential_provider = response.parse() + assert_matches_type(CredentialProvider, credential_provider, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: Kernel) -> None: + with client.credential_providers.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + credential_provider = response.parse() + assert_matches_type(CredentialProvider, credential_provider, 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: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.credential_providers.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_update(self, client: Kernel) -> None: + credential_provider = client.credential_providers.update( + id="id", + ) + assert_matches_type(CredentialProvider, credential_provider, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_update_with_all_params(self, client: Kernel) -> None: + credential_provider = client.credential_providers.update( + id="id", + token="ops_eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", + cache_ttl_seconds=300, + enabled=True, + priority=0, + ) + assert_matches_type(CredentialProvider, credential_provider, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_update(self, client: Kernel) -> None: + response = client.credential_providers.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + credential_provider = response.parse() + assert_matches_type(CredentialProvider, credential_provider, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_update(self, client: Kernel) -> None: + with client.credential_providers.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + credential_provider = response.parse() + assert_matches_type(CredentialProvider, credential_provider, 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: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.credential_providers.with_raw_response.update( + id="", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_list(self, client: Kernel) -> None: + credential_provider = client.credential_providers.list() + assert_matches_type(CredentialProviderListResponse, credential_provider, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_list(self, client: Kernel) -> None: + response = client.credential_providers.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + credential_provider = response.parse() + assert_matches_type(CredentialProviderListResponse, credential_provider, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_list(self, client: Kernel) -> None: + with client.credential_providers.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + credential_provider = response.parse() + assert_matches_type(CredentialProviderListResponse, credential_provider, 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: Kernel) -> None: + credential_provider = client.credential_providers.delete( + "id", + ) + assert credential_provider is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_delete(self, client: Kernel) -> None: + response = client.credential_providers.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + credential_provider = response.parse() + assert credential_provider is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_delete(self, client: Kernel) -> None: + with client.credential_providers.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + credential_provider = response.parse() + assert credential_provider 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: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.credential_providers.with_raw_response.delete( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_test(self, client: Kernel) -> None: + credential_provider = client.credential_providers.test( + "id", + ) + assert_matches_type(CredentialProviderTestResult, credential_provider, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_test(self, client: Kernel) -> None: + response = client.credential_providers.with_raw_response.test( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + credential_provider = response.parse() + assert_matches_type(CredentialProviderTestResult, credential_provider, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_test(self, client: Kernel) -> None: + with client.credential_providers.with_streaming_response.test( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + credential_provider = response.parse() + assert_matches_type(CredentialProviderTestResult, credential_provider, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_test(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.credential_providers.with_raw_response.test( + "", + ) + + +class TestAsyncCredentialProviders: + 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: AsyncKernel) -> None: + credential_provider = await async_client.credential_providers.create( + token="ops_eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", + provider_type="onepassword", + ) + assert_matches_type(CredentialProvider, credential_provider, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: + credential_provider = await async_client.credential_providers.create( + token="ops_eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", + provider_type="onepassword", + cache_ttl_seconds=300, + ) + assert_matches_type(CredentialProvider, credential_provider, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_create(self, async_client: AsyncKernel) -> None: + response = await async_client.credential_providers.with_raw_response.create( + token="ops_eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", + provider_type="onepassword", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + credential_provider = await response.parse() + assert_matches_type(CredentialProvider, credential_provider, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: + async with async_client.credential_providers.with_streaming_response.create( + token="ops_eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", + provider_type="onepassword", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + credential_provider = await response.parse() + assert_matches_type(CredentialProvider, credential_provider, 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: AsyncKernel) -> None: + credential_provider = await async_client.credential_providers.retrieve( + "id", + ) + assert_matches_type(CredentialProvider, credential_provider, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: + response = await async_client.credential_providers.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + credential_provider = await response.parse() + assert_matches_type(CredentialProvider, credential_provider, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: + async with async_client.credential_providers.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + credential_provider = await response.parse() + assert_matches_type(CredentialProvider, credential_provider, 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: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.credential_providers.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_update(self, async_client: AsyncKernel) -> None: + credential_provider = await async_client.credential_providers.update( + id="id", + ) + assert_matches_type(CredentialProvider, credential_provider, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> None: + credential_provider = await async_client.credential_providers.update( + id="id", + token="ops_eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", + cache_ttl_seconds=300, + enabled=True, + priority=0, + ) + assert_matches_type(CredentialProvider, credential_provider, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_update(self, async_client: AsyncKernel) -> None: + response = await async_client.credential_providers.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + credential_provider = await response.parse() + assert_matches_type(CredentialProvider, credential_provider, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_update(self, async_client: AsyncKernel) -> None: + async with async_client.credential_providers.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + credential_provider = await response.parse() + assert_matches_type(CredentialProvider, credential_provider, 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: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.credential_providers.with_raw_response.update( + id="", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncKernel) -> None: + credential_provider = await async_client.credential_providers.list() + assert_matches_type(CredentialProviderListResponse, credential_provider, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncKernel) -> None: + response = await async_client.credential_providers.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + credential_provider = await response.parse() + assert_matches_type(CredentialProviderListResponse, credential_provider, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: + async with async_client.credential_providers.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + credential_provider = await response.parse() + assert_matches_type(CredentialProviderListResponse, credential_provider, 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: AsyncKernel) -> None: + credential_provider = await async_client.credential_providers.delete( + "id", + ) + assert credential_provider is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: + response = await async_client.credential_providers.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + credential_provider = await response.parse() + assert credential_provider is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None: + async with async_client.credential_providers.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + credential_provider = await response.parse() + assert credential_provider 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: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.credential_providers.with_raw_response.delete( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_test(self, async_client: AsyncKernel) -> None: + credential_provider = await async_client.credential_providers.test( + "id", + ) + assert_matches_type(CredentialProviderTestResult, credential_provider, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_test(self, async_client: AsyncKernel) -> None: + response = await async_client.credential_providers.with_raw_response.test( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + credential_provider = await response.parse() + assert_matches_type(CredentialProviderTestResult, credential_provider, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_test(self, async_client: AsyncKernel) -> None: + async with async_client.credential_providers.with_streaming_response.test( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + credential_provider = await response.parse() + assert_matches_type(CredentialProviderTestResult, credential_provider, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_test(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.credential_providers.with_raw_response.test( + "", + ) From 81400b21127a548ec4567e12239359104560465b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 05:23:08 +0000 Subject: [PATCH 269/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index caf5ca3f..59acac47 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.26.0" + ".": "0.27.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index cad7dd22..7ac03dca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.26.0" +version = "0.27.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 0c46e78a..b962ca53 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.26.0" # x-release-please-version +__version__ = "0.27.0" # x-release-please-version From cd7b797d95b1a99ef4d14349458087eb19b93117 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 19:03:14 +0000 Subject: [PATCH 270/448] feat: Allow hot loading profiles into sessions --- .stats.yml | 4 +-- src/kernel/resources/browsers/browsers.py | 42 ++++++++++++++++++----- src/kernel/types/browser_update_params.py | 12 +++++++ tests/api_resources/test_browsers.py | 20 +++++++++++ 4 files changed, 68 insertions(+), 10 deletions(-) diff --git a/.stats.yml b/.stats.yml index 50043a40..bcbec15d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 97 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-7427d4bcaba5cad07910da7a222bdd2650b5280e6b889132ed38d230adafb8a5.yml -openapi_spec_hash: e8e3dc1ae54666d544d1fc848b25e7cf +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-d430a8e3407ceb608d912cabadbcb016b4fcf057ca56b3bbd179ea3b3121b484.yml +openapi_spec_hash: 8adbf013baf77abacaf04ed067749397 config_hash: b470456b217bb9502f5212311d395a6f diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index d835f7ac..534ae062 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -270,7 +270,9 @@ def update( self, id: str, *, + profile: BrowserProfile | Omit = omit, proxy_id: Optional[str] | Omit = omit, + viewport: BrowserViewport | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -278,14 +280,18 @@ def update( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> BrowserUpdateResponse: - """Update a browser session. + """ + Update a browser session. Args: - proxy_id: ID of the proxy to use. + profile: Profile to load into the browser session. Only allowed if the session does not + already have a profile loaded. - Omit to leave unchanged, set to empty string to remove + proxy_id: ID of the proxy to use. Omit to leave unchanged, set to empty string to remove proxy. + viewport: Viewport configuration to apply to the browser session. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -298,7 +304,14 @@ def update( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._patch( f"/browsers/{id}", - body=maybe_transform({"proxy_id": proxy_id}, browser_update_params.BrowserUpdateParams), + body=maybe_transform( + { + "profile": profile, + "proxy_id": proxy_id, + "viewport": viewport, + }, + browser_update_params.BrowserUpdateParams, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -668,7 +681,9 @@ async def update( self, id: str, *, + profile: BrowserProfile | Omit = omit, proxy_id: Optional[str] | Omit = omit, + viewport: BrowserViewport | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -676,14 +691,18 @@ async def update( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> BrowserUpdateResponse: - """Update a browser session. + """ + Update a browser session. Args: - proxy_id: ID of the proxy to use. + profile: Profile to load into the browser session. Only allowed if the session does not + already have a profile loaded. - Omit to leave unchanged, set to empty string to remove + proxy_id: ID of the proxy to use. Omit to leave unchanged, set to empty string to remove proxy. + viewport: Viewport configuration to apply to the browser session. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -696,7 +715,14 @@ async def update( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._patch( f"/browsers/{id}", - body=await async_maybe_transform({"proxy_id": proxy_id}, browser_update_params.BrowserUpdateParams), + body=await async_maybe_transform( + { + "profile": profile, + "proxy_id": proxy_id, + "viewport": viewport, + }, + browser_update_params.BrowserUpdateParams, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/kernel/types/browser_update_params.py b/src/kernel/types/browser_update_params.py index 72e02870..917cd7da 100644 --- a/src/kernel/types/browser_update_params.py +++ b/src/kernel/types/browser_update_params.py @@ -5,12 +5,24 @@ from typing import Optional from typing_extensions import TypedDict +from .shared_params.browser_profile import BrowserProfile +from .shared_params.browser_viewport import BrowserViewport + __all__ = ["BrowserUpdateParams"] class BrowserUpdateParams(TypedDict, total=False): + profile: BrowserProfile + """Profile to load into the browser session. + + Only allowed if the session does not already have a profile loaded. + """ + proxy_id: Optional[str] """ID of the proxy to use. Omit to leave unchanged, set to empty string to remove proxy. """ + + viewport: BrowserViewport + """Viewport configuration to apply to the browser session.""" diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index 139cf143..914b5af6 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -147,7 +147,17 @@ def test_method_update(self, client: Kernel) -> None: def test_method_update_with_all_params(self, client: Kernel) -> None: browser = client.browsers.update( id="htzv5orfit78e1m2biiifpbv", + profile={ + "id": "id", + "name": "name", + "save_changes": True, + }, proxy_id="proxy_id", + viewport={ + "height": 800, + "width": 1280, + "refresh_rate": 60, + }, ) assert_matches_type(BrowserUpdateResponse, browser, path=["response"]) @@ -498,7 +508,17 @@ async def test_method_update(self, async_client: AsyncKernel) -> None: async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> None: browser = await async_client.browsers.update( id="htzv5orfit78e1m2biiifpbv", + profile={ + "id": "id", + "name": "name", + "save_changes": True, + }, proxy_id="proxy_id", + viewport={ + "height": 800, + "width": 1280, + "refresh_rate": 60, + }, ) assert_matches_type(BrowserUpdateResponse, browser, path=["response"]) From 033968d402955ee9ccde1130659194f47197cc3f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 19:08:32 +0000 Subject: [PATCH 271/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 59acac47..8935e932 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.27.0" + ".": "0.28.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 7ac03dca..4402e482 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.27.0" +version = "0.28.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index b962ca53..f78bed97 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.27.0" # x-release-please-version +__version__ = "0.28.0" # x-release-please-version From 6ebe70eeaa3aaf931c6f811071be64e529f64ed4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 24 Jan 2026 04:16:33 +0000 Subject: [PATCH 272/448] chore(ci): upgrade `actions/github-script` --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc3b2d2d..b438aafe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,7 +63,7 @@ jobs: - name: Get GitHub OIDC Token if: github.repository == 'stainless-sdks/kernel-python' id: github-oidc - uses: actions/github-script@v6 + uses: actions/github-script@v8 with: script: core.setOutput('github_token', await core.getIDToken()); From da10d7eb75a34af3306212df1f18271b953309e3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 20:10:44 +0000 Subject: [PATCH 273/448] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index bcbec15d..48f30f8e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 97 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-d430a8e3407ceb608d912cabadbcb016b4fcf057ca56b3bbd179ea3b3121b484.yml -openapi_spec_hash: 8adbf013baf77abacaf04ed067749397 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-6c4fc624f12ead2f790c0f12148bc78f25d0b720080cae878b8a0fc0b5e5cd93.yml +openapi_spec_hash: ab25e882fbeeb782507576a442c1e15b config_hash: b470456b217bb9502f5212311d395a6f From fa319f2cee282740195f2a9e7fe24a396b8794ee Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 18:53:38 +0000 Subject: [PATCH 274/448] feat: add support for 1280x800@60 viewport --- .stats.yml | 4 ++-- src/kernel/resources/browser_pools.py | 16 ++++++++-------- src/kernel/resources/browsers/browsers.py | 8 ++++---- src/kernel/types/browser_create_params.py | 8 ++++---- src/kernel/types/browser_create_response.py | 8 ++++---- src/kernel/types/browser_list_response.py | 8 ++++---- src/kernel/types/browser_pool.py | 8 ++++---- .../types/browser_pool_acquire_response.py | 8 ++++---- src/kernel/types/browser_pool_create_params.py | 8 ++++---- src/kernel/types/browser_pool_update_params.py | 8 ++++---- src/kernel/types/browser_retrieve_response.py | 8 ++++---- src/kernel/types/browser_update_response.py | 8 ++++---- src/kernel/types/shared/browser_viewport.py | 2 +- .../types/shared_params/browser_viewport.py | 2 +- 14 files changed, 52 insertions(+), 52 deletions(-) diff --git a/.stats.yml b/.stats.yml index 48f30f8e..470e4983 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 97 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-6c4fc624f12ead2f790c0f12148bc78f25d0b720080cae878b8a0fc0b5e5cd93.yml -openapi_spec_hash: ab25e882fbeeb782507576a442c1e15b +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-46c8320dcd9f8fc596f469ef0dd1aafaca591ab36cf2a6f8a7297dc9136bdc71.yml +openapi_spec_hash: 1be1e6589cd94c581b241720e01a65bc config_hash: b470456b217bb9502f5212311d395a6f diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py index 884b0e16..4f6ad8b2 100644 --- a/src/kernel/resources/browser_pools.py +++ b/src/kernel/resources/browser_pools.py @@ -111,8 +111,8 @@ def create( image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, - 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will be - automatically determined from the width and height if they match a supported + 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will + be automatically determined from the width and height if they match a supported configuration exactly. Note: Higher resolutions may affect the responsiveness of live view browser @@ -243,8 +243,8 @@ def update( image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, - 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will be - automatically determined from the width and height if they match a supported + 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will + be automatically determined from the width and height if they match a supported configuration exactly. Note: Higher resolutions may affect the responsiveness of live view browser @@ -549,8 +549,8 @@ async def create( image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, - 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will be - automatically determined from the width and height if they match a supported + 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will + be automatically determined from the width and height if they match a supported configuration exactly. Note: Higher resolutions may affect the responsiveness of live view browser @@ -681,8 +681,8 @@ async def update( image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, - 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will be - automatically determined from the width and height if they match a supported + 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will + be automatically determined from the width and height if they match a supported configuration exactly. Note: Higher resolutions may affect the responsiveness of live view browser diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 534ae062..4b61d8d8 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -188,8 +188,8 @@ def create( image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, - 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will be - automatically determined from the width and height if they match a supported + 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will + be automatically determined from the width and height if they match a supported configuration exactly. Note: Higher resolutions may affect the responsiveness of live view browser @@ -599,8 +599,8 @@ async def create( image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, - 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will be - automatically determined from the width and height if they match a supported + 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will + be automatically determined from the width and height if they match a supported configuration exactly. Note: Higher resolutions may affect the responsiveness of live view browser diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index 0818760c..1e93fa6d 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -72,8 +72,8 @@ class BrowserCreateParams(TypedDict, total=False): If omitted, image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will - be automatically determined from the width and height if they match a supported - configuration exactly. Note: Higher resolutions may affect the responsiveness of - live view browser + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not + provided, it will be automatically determined from the width and height if they + match a supported configuration exactly. Note: Higher resolutions may affect the + responsiveness of live view browser """ diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index efff854f..b6c28acf 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -57,8 +57,8 @@ class BrowserCreateResponse(BaseModel): If omitted, image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will - be automatically determined from the width and height if they match a supported - configuration exactly. Note: Higher resolutions may affect the responsiveness of - live view browser + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not + provided, it will be automatically determined from the width and height if they + match a supported configuration exactly. Note: Higher resolutions may affect the + responsiveness of live view browser """ diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index 3ce26488..d99546d5 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -57,8 +57,8 @@ class BrowserListResponse(BaseModel): If omitted, image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will - be automatically determined from the width and height if they match a supported - configuration exactly. Note: Higher resolutions may affect the responsiveness of - live view browser + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not + provided, it will be automatically determined from the width and height if they + match a supported configuration exactly. Note: Higher resolutions may affect the + responsiveness of live view browser """ diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py index b9142f7c..fbbf2cb6 100644 --- a/src/kernel/types/browser_pool.py +++ b/src/kernel/types/browser_pool.py @@ -73,10 +73,10 @@ class BrowserPoolConfig(BaseModel): If omitted, image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will - be automatically determined from the width and height if they match a supported - configuration exactly. Note: Higher resolutions may affect the responsiveness of - live view browser + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not + provided, it will be automatically determined from the width and height if they + match a supported configuration exactly. Note: Higher resolutions may affect the + responsiveness of live view browser """ diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py index 4b70a87f..3175b398 100644 --- a/src/kernel/types/browser_pool_acquire_response.py +++ b/src/kernel/types/browser_pool_acquire_response.py @@ -57,8 +57,8 @@ class BrowserPoolAcquireResponse(BaseModel): If omitted, image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will - be automatically determined from the width and height if they match a supported - configuration exactly. Note: Higher resolutions may affect the responsiveness of - live view browser + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not + provided, it will be automatically determined from the width and height if they + match a supported configuration exactly. Note: Higher resolutions may affect the + responsiveness of live view browser """ diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py index afa9e6e5..81deaa68 100644 --- a/src/kernel/types/browser_pool_create_params.py +++ b/src/kernel/types/browser_pool_create_params.py @@ -72,8 +72,8 @@ class BrowserPoolCreateParams(TypedDict, total=False): If omitted, image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will - be automatically determined from the width and height if they match a supported - configuration exactly. Note: Higher resolutions may affect the responsiveness of - live view browser + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not + provided, it will be automatically determined from the width and height if they + match a supported configuration exactly. Note: Higher resolutions may affect the + responsiveness of live view browser """ diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py index ac23916a..63487086 100644 --- a/src/kernel/types/browser_pool_update_params.py +++ b/src/kernel/types/browser_pool_update_params.py @@ -78,8 +78,8 @@ class BrowserPoolUpdateParams(TypedDict, total=False): If omitted, image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will - be automatically determined from the width and height if they match a supported - configuration exactly. Note: Higher resolutions may affect the responsiveness of - live view browser + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not + provided, it will be automatically determined from the width and height if they + match a supported configuration exactly. Note: Higher resolutions may affect the + responsiveness of live view browser """ diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 12f58a5e..09210e8c 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -57,8 +57,8 @@ class BrowserRetrieveResponse(BaseModel): If omitted, image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will - be automatically determined from the width and height if they match a supported - configuration exactly. Note: Higher resolutions may affect the responsiveness of - live view browser + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not + provided, it will be automatically determined from the width and height if they + match a supported configuration exactly. Note: Higher resolutions may affect the + responsiveness of live view browser """ diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py index d5cb3150..01c34be5 100644 --- a/src/kernel/types/browser_update_response.py +++ b/src/kernel/types/browser_update_response.py @@ -57,8 +57,8 @@ class BrowserUpdateResponse(BaseModel): If omitted, image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will - be automatically determined from the width and height if they match a supported - configuration exactly. Note: Higher resolutions may affect the responsiveness of - live view browser + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not + provided, it will be automatically determined from the width and height if they + match a supported configuration exactly. Note: Higher resolutions may affect the + responsiveness of live view browser """ diff --git a/src/kernel/types/shared/browser_viewport.py b/src/kernel/types/shared/browser_viewport.py index ab8f4273..2329dd75 100644 --- a/src/kernel/types/shared/browser_viewport.py +++ b/src/kernel/types/shared/browser_viewport.py @@ -12,7 +12,7 @@ class BrowserViewport(BaseModel): If omitted, image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. - Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1024x768@60, 1200x800@60 + Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will be automatically determined from the width and height if they match a supported configuration exactly. Note: Higher resolutions may affect the responsiveness of live view browser """ diff --git a/src/kernel/types/shared_params/browser_viewport.py b/src/kernel/types/shared_params/browser_viewport.py index 9236547a..7041ea55 100644 --- a/src/kernel/types/shared_params/browser_viewport.py +++ b/src/kernel/types/shared_params/browser_viewport.py @@ -12,7 +12,7 @@ class BrowserViewport(TypedDict, total=False): If omitted, image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. - Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1024x768@60, 1200x800@60 + Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will be automatically determined from the width and height if they match a supported configuration exactly. Note: Higher resolutions may affect the responsiveness of live view browser """ From ae7fc70d2547997a2e388cff2d187082f37ada19 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 04:09:11 +0000 Subject: [PATCH 275/448] feat(client): add custom JSON encoder for extended type support --- src/kernel/_base_client.py | 7 +- src/kernel/_compat.py | 6 +- src/kernel/_utils/_json.py | 35 ++++++++++ tests/test_utils/test_json.py | 126 ++++++++++++++++++++++++++++++++++ 4 files changed, 169 insertions(+), 5 deletions(-) create mode 100644 src/kernel/_utils/_json.py create mode 100644 tests/test_utils/test_json.py diff --git a/src/kernel/_base_client.py b/src/kernel/_base_client.py index 07809c5b..a3d47eaf 100644 --- a/src/kernel/_base_client.py +++ b/src/kernel/_base_client.py @@ -86,6 +86,7 @@ APIConnectionError, APIResponseValidationError, ) +from ._utils._json import openapi_dumps log: logging.Logger = logging.getLogger(__name__) @@ -554,8 +555,10 @@ def _build_request( kwargs["content"] = options.content elif isinstance(json_data, bytes): kwargs["content"] = json_data - else: - kwargs["json"] = json_data if is_given(json_data) else None + elif not files: + # Don't set content when JSON is sent as multipart/form-data, + # since httpx's content param overrides other body arguments + kwargs["content"] = openapi_dumps(json_data) if is_given(json_data) and json_data is not None else None kwargs["files"] = files else: headers.pop("Content-Type", None) diff --git a/src/kernel/_compat.py b/src/kernel/_compat.py index bdef67f0..786ff42a 100644 --- a/src/kernel/_compat.py +++ b/src/kernel/_compat.py @@ -139,6 +139,7 @@ def model_dump( exclude_defaults: bool = False, warnings: bool = True, mode: Literal["json", "python"] = "python", + by_alias: bool | None = None, ) -> dict[str, Any]: if (not PYDANTIC_V1) or hasattr(model, "model_dump"): return model.model_dump( @@ -148,13 +149,12 @@ def model_dump( exclude_defaults=exclude_defaults, # warnings are not supported in Pydantic v1 warnings=True if PYDANTIC_V1 else warnings, + by_alias=by_alias, ) return cast( "dict[str, Any]", model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast] - exclude=exclude, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, + exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, by_alias=bool(by_alias) ), ) diff --git a/src/kernel/_utils/_json.py b/src/kernel/_utils/_json.py new file mode 100644 index 00000000..60584214 --- /dev/null +++ b/src/kernel/_utils/_json.py @@ -0,0 +1,35 @@ +import json +from typing import Any +from datetime import datetime +from typing_extensions import override + +import pydantic + +from .._compat import model_dump + + +def openapi_dumps(obj: Any) -> bytes: + """ + Serialize an object to UTF-8 encoded JSON bytes. + + Extends the standard json.dumps with support for additional types + commonly used in the SDK, such as `datetime`, `pydantic.BaseModel`, etc. + """ + return json.dumps( + obj, + cls=_CustomEncoder, + # Uses the same defaults as httpx's JSON serialization + ensure_ascii=False, + separators=(",", ":"), + allow_nan=False, + ).encode() + + +class _CustomEncoder(json.JSONEncoder): + @override + def default(self, o: Any) -> Any: + if isinstance(o, datetime): + return o.isoformat() + if isinstance(o, pydantic.BaseModel): + return model_dump(o, exclude_unset=True, mode="json", by_alias=True) + return super().default(o) diff --git a/tests/test_utils/test_json.py b/tests/test_utils/test_json.py new file mode 100644 index 00000000..20e385b9 --- /dev/null +++ b/tests/test_utils/test_json.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +import datetime +from typing import Union + +import pydantic + +from kernel import _compat +from kernel._utils._json import openapi_dumps + + +class TestOpenapiDumps: + def test_basic(self) -> None: + data = {"key": "value", "number": 42} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"key":"value","number":42}' + + def test_datetime_serialization(self) -> None: + dt = datetime.datetime(2023, 1, 1, 12, 0, 0) + data = {"datetime": dt} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"datetime":"2023-01-01T12:00:00"}' + + def test_pydantic_model_serialization(self) -> None: + class User(pydantic.BaseModel): + first_name: str + last_name: str + age: int + + model_instance = User(first_name="John", last_name="Kramer", age=83) + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"first_name":"John","last_name":"Kramer","age":83}}' + + def test_pydantic_model_with_default_values(self) -> None: + class User(pydantic.BaseModel): + name: str + role: str = "user" + active: bool = True + score: int = 0 + + model_instance = User(name="Alice") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Alice"}}' + + def test_pydantic_model_with_default_values_overridden(self) -> None: + class User(pydantic.BaseModel): + name: str + role: str = "user" + active: bool = True + + model_instance = User(name="Bob", role="admin", active=False) + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Bob","role":"admin","active":false}}' + + def test_pydantic_model_with_alias(self) -> None: + class User(pydantic.BaseModel): + first_name: str = pydantic.Field(alias="firstName") + last_name: str = pydantic.Field(alias="lastName") + + model_instance = User(firstName="John", lastName="Doe") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"firstName":"John","lastName":"Doe"}}' + + def test_pydantic_model_with_alias_and_default(self) -> None: + class User(pydantic.BaseModel): + user_name: str = pydantic.Field(alias="userName") + user_role: str = pydantic.Field(default="member", alias="userRole") + is_active: bool = pydantic.Field(default=True, alias="isActive") + + model_instance = User(userName="charlie") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"userName":"charlie"}}' + + model_with_overrides = User(userName="diana", userRole="admin", isActive=False) + data = {"model": model_with_overrides} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"userName":"diana","userRole":"admin","isActive":false}}' + + def test_pydantic_model_with_nested_models_and_defaults(self) -> None: + class Address(pydantic.BaseModel): + street: str + city: str = "Unknown" + + class User(pydantic.BaseModel): + name: str + address: Address + verified: bool = False + + if _compat.PYDANTIC_V1: + # to handle forward references in Pydantic v1 + User.update_forward_refs(**locals()) # type: ignore[reportDeprecated] + + address = Address(street="123 Main St") + user = User(name="Diana", address=address) + data = {"user": user} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"user":{"name":"Diana","address":{"street":"123 Main St"}}}' + + address_with_city = Address(street="456 Oak Ave", city="Boston") + user_verified = User(name="Eve", address=address_with_city, verified=True) + data = {"user": user_verified} + json_bytes = openapi_dumps(data) + assert ( + json_bytes == b'{"user":{"name":"Eve","address":{"street":"456 Oak Ave","city":"Boston"},"verified":true}}' + ) + + def test_pydantic_model_with_optional_fields(self) -> None: + class User(pydantic.BaseModel): + name: str + email: Union[str, None] + phone: Union[str, None] + + model_with_none = User(name="Eve", email=None, phone=None) + data = {"model": model_with_none} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Eve","email":null,"phone":null}}' + + model_with_values = User(name="Frank", email="frank@example.com", phone=None) + data = {"model": model_with_values} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Frank","email":"frank@example.com","phone":null}}' From 56e3c2a0526f02e9704704513d7f5efc4304b65e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 05:05:38 +0000 Subject: [PATCH 276/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 8935e932..b8dda9bf 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.28.0" + ".": "0.29.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 4402e482..66433700 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.28.0" +version = "0.29.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index f78bed97..d24a1a6c 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.28.0" # x-release-please-version +__version__ = "0.29.0" # x-release-please-version From 047de44b9a12d75e247681f5d4eddf02305ce6a8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 18:01:52 +0000 Subject: [PATCH 277/448] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 470e4983..67f01853 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 97 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-46c8320dcd9f8fc596f469ef0dd1aafaca591ab36cf2a6f8a7297dc9136bdc71.yml -openapi_spec_hash: 1be1e6589cd94c581b241720e01a65bc +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-c955031ac0bb644c0aa1d1120e288f703168abeb0e44a166f55119295f737cc5.yml +openapi_spec_hash: 9fbc24cdaccdd21264e995df40b52d3f config_hash: b470456b217bb9502f5212311d395a6f From e90e8b711162b1742a80d21e00f8ed8d683865fe Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 14:58:42 +0000 Subject: [PATCH 278/448] feat: Neil/kernel 872 templates v3 --- .stats.yml | 8 +- api.md | 2 + src/kernel/resources/invocations.py | 79 +++++++++++++++++ src/kernel/types/__init__.py | 1 + .../invocation_list_browsers_response.py | 68 +++++++++++++++ tests/api_resources/test_invocations.py | 85 +++++++++++++++++++ 6 files changed, 239 insertions(+), 4 deletions(-) create mode 100644 src/kernel/types/invocation_list_browsers_response.py diff --git a/.stats.yml b/.stats.yml index 67f01853..710d8bda 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 97 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-c955031ac0bb644c0aa1d1120e288f703168abeb0e44a166f55119295f737cc5.yml -openapi_spec_hash: 9fbc24cdaccdd21264e995df40b52d3f -config_hash: b470456b217bb9502f5212311d395a6f +configured_endpoints: 98 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-ccbe854895eb34a9562e33979f5f43cd6ad1f529d5924ee56e56f0c94dcf0454.yml +openapi_spec_hash: 2fa4ecbe742fc46fdde481188c1d885e +config_hash: dd218aae3f852dff79e77febc2077b8e diff --git a/api.md b/api.md index e951a78a..82116657 100644 --- a/api.md +++ b/api.md @@ -59,6 +59,7 @@ from kernel.types import ( InvocationUpdateResponse, InvocationListResponse, InvocationFollowResponse, + InvocationListBrowsersResponse, ) ``` @@ -70,6 +71,7 @@ Methods: - client.invocations.list(\*\*params) -> SyncOffsetPagination[InvocationListResponse] - client.invocations.delete_browsers(id) -> None - client.invocations.follow(id, \*\*params) -> InvocationFollowResponse +- client.invocations.list_browsers(id) -> InvocationListBrowsersResponse # Browsers diff --git a/src/kernel/resources/invocations.py b/src/kernel/resources/invocations.py index 3b812d45..3194026d 100644 --- a/src/kernel/resources/invocations.py +++ b/src/kernel/resources/invocations.py @@ -26,6 +26,7 @@ from ..types.invocation_follow_response import InvocationFollowResponse from ..types.invocation_update_response import InvocationUpdateResponse from ..types.invocation_retrieve_response import InvocationRetrieveResponse +from ..types.invocation_list_browsers_response import InvocationListBrowsersResponse __all__ = ["InvocationsResource", "AsyncInvocationsResource"] @@ -347,6 +348,39 @@ def follow( stream_cls=Stream[InvocationFollowResponse], ) + def list_browsers( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> InvocationListBrowsersResponse: + """ + Returns all active browser sessions created within the specified invocation. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/invocations/{id}/browsers", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=InvocationListBrowsersResponse, + ) + class AsyncInvocationsResource(AsyncAPIResource): @cached_property @@ -665,6 +699,39 @@ async def follow( stream_cls=AsyncStream[InvocationFollowResponse], ) + async def list_browsers( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> InvocationListBrowsersResponse: + """ + Returns all active browser sessions created within the specified invocation. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/invocations/{id}/browsers", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=InvocationListBrowsersResponse, + ) + class InvocationsResourceWithRawResponse: def __init__(self, invocations: InvocationsResource) -> None: @@ -688,6 +755,9 @@ def __init__(self, invocations: InvocationsResource) -> None: self.follow = to_raw_response_wrapper( invocations.follow, ) + self.list_browsers = to_raw_response_wrapper( + invocations.list_browsers, + ) class AsyncInvocationsResourceWithRawResponse: @@ -712,6 +782,9 @@ def __init__(self, invocations: AsyncInvocationsResource) -> None: self.follow = async_to_raw_response_wrapper( invocations.follow, ) + self.list_browsers = async_to_raw_response_wrapper( + invocations.list_browsers, + ) class InvocationsResourceWithStreamingResponse: @@ -736,6 +809,9 @@ def __init__(self, invocations: InvocationsResource) -> None: self.follow = to_streamed_response_wrapper( invocations.follow, ) + self.list_browsers = to_streamed_response_wrapper( + invocations.list_browsers, + ) class AsyncInvocationsResourceWithStreamingResponse: @@ -760,3 +836,6 @@ def __init__(self, invocations: AsyncInvocationsResource) -> None: self.follow = async_to_streamed_response_wrapper( invocations.follow, ) + self.list_browsers = async_to_streamed_response_wrapper( + invocations.list_browsers, + ) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index ef16ae24..7e3ea2d6 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -74,6 +74,7 @@ from .credential_provider_create_params import CredentialProviderCreateParams as CredentialProviderCreateParams from .credential_provider_list_response import CredentialProviderListResponse as CredentialProviderListResponse from .credential_provider_update_params import CredentialProviderUpdateParams as CredentialProviderUpdateParams +from .invocation_list_browsers_response import InvocationListBrowsersResponse as InvocationListBrowsersResponse from .extension_download_from_chrome_store_params import ( ExtensionDownloadFromChromeStoreParams as ExtensionDownloadFromChromeStoreParams, ) diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py new file mode 100644 index 00000000..4d41a298 --- /dev/null +++ b/src/kernel/types/invocation_list_browsers_response.py @@ -0,0 +1,68 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from datetime import datetime + +from .profile import Profile +from .._models import BaseModel +from .browser_persistence import BrowserPersistence +from .shared.browser_viewport import BrowserViewport + +__all__ = ["InvocationListBrowsersResponse", "Browser"] + + +class Browser(BaseModel): + cdp_ws_url: str + """Websocket URL for Chrome DevTools Protocol connections to the browser session""" + + created_at: datetime + """When the browser session was created.""" + + headless: bool + """Whether the browser session is running in headless mode.""" + + session_id: str + """Unique identifier for the browser session""" + + stealth: bool + """Whether the browser session is running in stealth mode.""" + + timeout_seconds: int + """The number of seconds of inactivity before the browser session is terminated.""" + + browser_live_view_url: Optional[str] = None + """Remote URL for live viewing the browser session. + + Only available for non-headless browsers. + """ + + deleted_at: Optional[datetime] = None + """When the browser session was soft-deleted. Only present for deleted sessions.""" + + kiosk_mode: Optional[bool] = None + """Whether the browser session is running in kiosk mode.""" + + persistence: Optional[BrowserPersistence] = None + """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead.""" + + profile: Optional[Profile] = None + """Browser profile metadata.""" + + proxy_id: Optional[str] = None + """ID of the proxy associated with this browser session, if any.""" + + viewport: Optional[BrowserViewport] = None + """Initial browser window size in pixels with optional refresh rate. + + If omitted, image defaults apply (1920x1080@25). Only specific viewport + configurations are supported. The server will reject unsupported combinations. + Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not + provided, it will be automatically determined from the width and height if they + match a supported configuration exactly. Note: Higher resolutions may affect the + responsiveness of live view browser + """ + + +class InvocationListBrowsersResponse(BaseModel): + browsers: List[Browser] diff --git a/tests/api_resources/test_invocations.py b/tests/api_resources/test_invocations.py index 40c05453..d870adce 100644 --- a/tests/api_resources/test_invocations.py +++ b/tests/api_resources/test_invocations.py @@ -14,6 +14,7 @@ InvocationCreateResponse, InvocationUpdateResponse, InvocationRetrieveResponse, + InvocationListBrowsersResponse, ) from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination @@ -309,6 +310,48 @@ def test_path_params_follow(self, client: Kernel) -> None: id="", ) + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_list_browsers(self, client: Kernel) -> None: + invocation = client.invocations.list_browsers( + "id", + ) + assert_matches_type(InvocationListBrowsersResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_list_browsers(self, client: Kernel) -> None: + response = client.invocations.with_raw_response.list_browsers( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + invocation = response.parse() + assert_matches_type(InvocationListBrowsersResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_list_browsers(self, client: Kernel) -> None: + with client.invocations.with_streaming_response.list_browsers( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + invocation = response.parse() + assert_matches_type(InvocationListBrowsersResponse, invocation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_list_browsers(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.invocations.with_raw_response.list_browsers( + "", + ) + class TestAsyncInvocations: parametrize = pytest.mark.parametrize( @@ -600,3 +643,45 @@ async def test_path_params_follow(self, async_client: AsyncKernel) -> None: await async_client.invocations.with_raw_response.follow( id="", ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_list_browsers(self, async_client: AsyncKernel) -> None: + invocation = await async_client.invocations.list_browsers( + "id", + ) + assert_matches_type(InvocationListBrowsersResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_list_browsers(self, async_client: AsyncKernel) -> None: + response = await async_client.invocations.with_raw_response.list_browsers( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + invocation = await response.parse() + assert_matches_type(InvocationListBrowsersResponse, invocation, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_list_browsers(self, async_client: AsyncKernel) -> None: + async with async_client.invocations.with_streaming_response.list_browsers( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + invocation = await response.parse() + assert_matches_type(InvocationListBrowsersResponse, invocation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_list_browsers(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.invocations.with_raw_response.list_browsers( + "", + ) From 5edc08036e6069d8e4fbf4af05142c8f15df40ed Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:59:43 +0000 Subject: [PATCH 279/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index b8dda9bf..554e34bb 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.29.0" + ".": "0.30.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 66433700..16affc3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.29.0" +version = "0.30.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index d24a1a6c..e08b152b 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.29.0" # x-release-please-version +__version__ = "0.30.0" # x-release-please-version From 0f512e27b3ea8fb215ab3d6898cb0abfb21a3772 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 12:38:42 +0000 Subject: [PATCH 280/448] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 710d8bda..28bf5610 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 98 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-ccbe854895eb34a9562e33979f5f43cd6ad1f529d5924ee56e56f0c94dcf0454.yml -openapi_spec_hash: 2fa4ecbe742fc46fdde481188c1d885e +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-652441005cba60f2ada5328d8dfc60165f6ab0bb271d6b64feca250fe73dc197.yml +openapi_spec_hash: f50a08ba2578efd8b756bdd920510c50 config_hash: dd218aae3f852dff79e77febc2077b8e From 7644a5d672ff840713e46e74d56b4fdf52e57397 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 18:44:48 +0000 Subject: [PATCH 281/448] feat: add batch computer action proxy endpoint --- .stats.yml | 8 +- api.md | 7 +- src/kernel/resources/browsers/computer.py | 172 +++++++++++++++++ src/kernel/types/browsers/__init__.py | 2 + .../types/browsers/computer_batch_params.py | 170 +++++++++++++++++ .../computer_get_mouse_position_response.py | 13 ++ tests/api_resources/browsers/test_computer.py | 177 ++++++++++++++++++ 7 files changed, 544 insertions(+), 5 deletions(-) create mode 100644 src/kernel/types/browsers/computer_batch_params.py create mode 100644 src/kernel/types/browsers/computer_get_mouse_position_response.py diff --git a/.stats.yml b/.stats.yml index 28bf5610..91fd3b68 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 98 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-652441005cba60f2ada5328d8dfc60165f6ab0bb271d6b64feca250fe73dc197.yml -openapi_spec_hash: f50a08ba2578efd8b756bdd920510c50 -config_hash: dd218aae3f852dff79e77febc2077b8e +configured_endpoints: 100 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-a6d93dc291278035c96add38bb6150ec2b9ba8bbabb4676e3dbbb8444cf3b1e4.yml +openapi_spec_hash: 694bcc56d94fd0ff0d1f7b0fc1dae8ba +config_hash: 62e33cf2ed8fe0b4ceebba63367481ad diff --git a/api.md b/api.md index 82116657..d55fecff 100644 --- a/api.md +++ b/api.md @@ -187,14 +187,19 @@ Methods: Types: ```python -from kernel.types.browsers import ComputerSetCursorVisibilityResponse +from kernel.types.browsers import ( + ComputerGetMousePositionResponse, + ComputerSetCursorVisibilityResponse, +) ``` Methods: +- client.browsers.computer.batch(id, \*\*params) -> None - client.browsers.computer.capture_screenshot(id, \*\*params) -> BinaryAPIResponse - client.browsers.computer.click_mouse(id, \*\*params) -> None - client.browsers.computer.drag_mouse(id, \*\*params) -> None +- client.browsers.computer.get_mouse_position(id) -> ComputerGetMousePositionResponse - client.browsers.computer.move_mouse(id, \*\*params) -> None - client.browsers.computer.press_key(id, \*\*params) -> None - client.browsers.computer.scroll(id, \*\*params) -> None diff --git a/src/kernel/resources/browsers/computer.py b/src/kernel/resources/browsers/computer.py index c23dd3db..933767f0 100644 --- a/src/kernel/resources/browsers/computer.py +++ b/src/kernel/resources/browsers/computer.py @@ -27,6 +27,7 @@ ) from ..._base_client import make_request_options from ...types.browsers import ( + computer_batch_params, computer_scroll_params, computer_press_key_params, computer_type_text_params, @@ -36,6 +37,7 @@ computer_capture_screenshot_params, computer_set_cursor_visibility_params, ) +from ...types.browsers.computer_get_mouse_position_response import ComputerGetMousePositionResponse from ...types.browsers.computer_set_cursor_visibility_response import ComputerSetCursorVisibilityResponse __all__ = ["ComputerResource", "AsyncComputerResource"] @@ -61,6 +63,46 @@ def with_streaming_response(self) -> ComputerResourceWithStreamingResponse: """ return ComputerResourceWithStreamingResponse(self) + def batch( + self, + id: str, + *, + actions: Iterable[computer_batch_params.Action], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via 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: + """ + Send an array of computer actions to execute in order on the browser instance. + Execution stops on the first error. This reduces network latency compared to + sending individual action requests. + + Args: + actions: Ordered list of actions to execute. Execution stops on the first error. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._post( + f"/browsers/{id}/computer/batch", + body=maybe_transform({"actions": actions}, computer_batch_params.ComputerBatchParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + def capture_screenshot( self, id: str, @@ -227,6 +269,39 @@ def drag_mouse( cast_to=NoneType, ) + def get_mouse_position( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ComputerGetMousePositionResponse: + """ + Get the current mouse cursor position on the browser instance + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/browsers/{id}/computer/get_mouse_position", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ComputerGetMousePositionResponse, + ) + def move_mouse( self, id: str, @@ -499,6 +574,46 @@ def with_streaming_response(self) -> AsyncComputerResourceWithStreamingResponse: """ return AsyncComputerResourceWithStreamingResponse(self) + async def batch( + self, + id: str, + *, + actions: Iterable[computer_batch_params.Action], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via 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: + """ + Send an array of computer actions to execute in order on the browser instance. + Execution stops on the first error. This reduces network latency compared to + sending individual action requests. + + Args: + actions: Ordered list of actions to execute. Execution stops on the first error. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._post( + f"/browsers/{id}/computer/batch", + body=await async_maybe_transform({"actions": actions}, computer_batch_params.ComputerBatchParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + async def capture_screenshot( self, id: str, @@ -665,6 +780,39 @@ async def drag_mouse( cast_to=NoneType, ) + async def get_mouse_position( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ComputerGetMousePositionResponse: + """ + Get the current mouse cursor position on the browser instance + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/browsers/{id}/computer/get_mouse_position", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ComputerGetMousePositionResponse, + ) + async def move_mouse( self, id: str, @@ -921,6 +1069,9 @@ class ComputerResourceWithRawResponse: def __init__(self, computer: ComputerResource) -> None: self._computer = computer + self.batch = to_raw_response_wrapper( + computer.batch, + ) self.capture_screenshot = to_custom_raw_response_wrapper( computer.capture_screenshot, BinaryAPIResponse, @@ -931,6 +1082,9 @@ def __init__(self, computer: ComputerResource) -> None: self.drag_mouse = to_raw_response_wrapper( computer.drag_mouse, ) + self.get_mouse_position = to_raw_response_wrapper( + computer.get_mouse_position, + ) self.move_mouse = to_raw_response_wrapper( computer.move_mouse, ) @@ -952,6 +1106,9 @@ class AsyncComputerResourceWithRawResponse: def __init__(self, computer: AsyncComputerResource) -> None: self._computer = computer + self.batch = async_to_raw_response_wrapper( + computer.batch, + ) self.capture_screenshot = async_to_custom_raw_response_wrapper( computer.capture_screenshot, AsyncBinaryAPIResponse, @@ -962,6 +1119,9 @@ def __init__(self, computer: AsyncComputerResource) -> None: self.drag_mouse = async_to_raw_response_wrapper( computer.drag_mouse, ) + self.get_mouse_position = async_to_raw_response_wrapper( + computer.get_mouse_position, + ) self.move_mouse = async_to_raw_response_wrapper( computer.move_mouse, ) @@ -983,6 +1143,9 @@ class ComputerResourceWithStreamingResponse: def __init__(self, computer: ComputerResource) -> None: self._computer = computer + self.batch = to_streamed_response_wrapper( + computer.batch, + ) self.capture_screenshot = to_custom_streamed_response_wrapper( computer.capture_screenshot, StreamedBinaryAPIResponse, @@ -993,6 +1156,9 @@ def __init__(self, computer: ComputerResource) -> None: self.drag_mouse = to_streamed_response_wrapper( computer.drag_mouse, ) + self.get_mouse_position = to_streamed_response_wrapper( + computer.get_mouse_position, + ) self.move_mouse = to_streamed_response_wrapper( computer.move_mouse, ) @@ -1014,6 +1180,9 @@ class AsyncComputerResourceWithStreamingResponse: def __init__(self, computer: AsyncComputerResource) -> None: self._computer = computer + self.batch = async_to_streamed_response_wrapper( + computer.batch, + ) self.capture_screenshot = async_to_custom_streamed_response_wrapper( computer.capture_screenshot, AsyncStreamedBinaryAPIResponse, @@ -1024,6 +1193,9 @@ def __init__(self, computer: AsyncComputerResource) -> None: self.drag_mouse = async_to_streamed_response_wrapper( computer.drag_mouse, ) + self.get_mouse_position = async_to_streamed_response_wrapper( + computer.get_mouse_position, + ) self.move_mouse = async_to_streamed_response_wrapper( computer.move_mouse, ) diff --git a/src/kernel/types/browsers/__init__.py b/src/kernel/types/browsers/__init__.py index e6b2eca3..3daee051 100644 --- a/src/kernel/types/browsers/__init__.py +++ b/src/kernel/types/browsers/__init__.py @@ -18,6 +18,7 @@ from .process_spawn_params import ProcessSpawnParams as ProcessSpawnParams from .process_stdin_params import ProcessStdinParams as ProcessStdinParams from .replay_list_response import ReplayListResponse as ReplayListResponse +from .computer_batch_params import ComputerBatchParams as ComputerBatchParams from .f_list_files_response import FListFilesResponse as FListFilesResponse from .process_exec_response import ProcessExecResponse as ProcessExecResponse from .process_kill_response import ProcessKillResponse as ProcessKillResponse @@ -41,6 +42,7 @@ from .f_set_file_permissions_params import FSetFilePermissionsParams as FSetFilePermissionsParams from .process_stdout_stream_response import ProcessStdoutStreamResponse as ProcessStdoutStreamResponse from .computer_capture_screenshot_params import ComputerCaptureScreenshotParams as ComputerCaptureScreenshotParams +from .computer_get_mouse_position_response import ComputerGetMousePositionResponse as ComputerGetMousePositionResponse from .computer_set_cursor_visibility_params import ( ComputerSetCursorVisibilityParams as ComputerSetCursorVisibilityParams, ) diff --git a/src/kernel/types/browsers/computer_batch_params.py b/src/kernel/types/browsers/computer_batch_params.py new file mode 100644 index 00000000..601cd2b9 --- /dev/null +++ b/src/kernel/types/browsers/computer_batch_params.py @@ -0,0 +1,170 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Iterable +from typing_extensions import Literal, Required, TypedDict + +from ..._types import SequenceNotStr + +__all__ = [ + "ComputerBatchParams", + "Action", + "ActionClickMouse", + "ActionDragMouse", + "ActionMoveMouse", + "ActionPressKey", + "ActionScroll", + "ActionSetCursor", + "ActionSleep", + "ActionTypeText", +] + + +class ComputerBatchParams(TypedDict, total=False): + actions: Required[Iterable[Action]] + """Ordered list of actions to execute. Execution stops on the first error.""" + + +class ActionClickMouse(TypedDict, total=False): + x: Required[int] + """X coordinate of the click position""" + + y: Required[int] + """Y coordinate of the click position""" + + button: Literal["left", "right", "middle", "back", "forward"] + """Mouse button to interact with""" + + click_type: Literal["down", "up", "click"] + """Type of click action""" + + hold_keys: SequenceNotStr[str] + """Modifier keys to hold during the click""" + + num_clicks: int + """Number of times to repeat the click""" + + +class ActionDragMouse(TypedDict, total=False): + path: Required[Iterable[Iterable[int]]] + """Ordered list of [x, y] coordinate pairs to move through while dragging. + + Must contain at least 2 points. + """ + + button: Literal["left", "middle", "right"] + """Mouse button to drag with""" + + delay: int + """Delay in milliseconds between button down and starting to move along the path.""" + + hold_keys: SequenceNotStr[str] + """Modifier keys to hold during the drag""" + + step_delay_ms: int + """ + Delay in milliseconds between relative steps while dragging (not the initial + delay). + """ + + steps_per_segment: int + """Number of relative move steps per segment in the path. Minimum 1.""" + + +class ActionMoveMouse(TypedDict, total=False): + x: Required[int] + """X coordinate to move the cursor to""" + + y: Required[int] + """Y coordinate to move the cursor to""" + + hold_keys: SequenceNotStr[str] + """Modifier keys to hold during the move""" + + +class ActionPressKey(TypedDict, total=False): + keys: Required[SequenceNotStr[str]] + """List of key symbols to press. + + Each item should be a key symbol supported by xdotool (see X11 keysym + definitions). Examples include "Return", "Shift", "Ctrl", "Alt", "F5". Items in + this list could also be combinations, e.g. "Ctrl+t" or "Ctrl+Shift+Tab". + """ + + duration: int + """Duration to hold the keys down in milliseconds. + + If omitted or 0, keys are tapped. + """ + + hold_keys: SequenceNotStr[str] + """Optional modifier keys to hold during the key press sequence.""" + + +class ActionScroll(TypedDict, total=False): + x: Required[int] + """X coordinate at which to perform the scroll""" + + y: Required[int] + """Y coordinate at which to perform the scroll""" + + delta_x: int + """Horizontal scroll amount. Positive scrolls right, negative scrolls left.""" + + delta_y: int + """Vertical scroll amount. Positive scrolls down, negative scrolls up.""" + + hold_keys: SequenceNotStr[str] + """Modifier keys to hold during the scroll""" + + +class ActionSetCursor(TypedDict, total=False): + hidden: Required[bool] + """Whether the cursor should be hidden or visible""" + + +class ActionSleep(TypedDict, total=False): + """Pause execution for a specified duration.""" + + duration_ms: Required[int] + """Duration to sleep in milliseconds.""" + + +class ActionTypeText(TypedDict, total=False): + text: Required[str] + """Text to type on the browser instance""" + + delay: int + """Delay in milliseconds between keystrokes""" + + +class Action(TypedDict, total=False): + """A single computer action to execute as part of a batch. + + The `type` field selects which + action to perform, and the corresponding field contains the action parameters. + Exactly one action field matching the type must be provided. + """ + + type: Required[ + Literal["click_mouse", "move_mouse", "type_text", "press_key", "scroll", "drag_mouse", "set_cursor", "sleep"] + ] + """The type of action to perform.""" + + click_mouse: ActionClickMouse + + drag_mouse: ActionDragMouse + + move_mouse: ActionMoveMouse + + press_key: ActionPressKey + + scroll: ActionScroll + + set_cursor: ActionSetCursor + + sleep: ActionSleep + """Pause execution for a specified duration.""" + + type_text: ActionTypeText diff --git a/src/kernel/types/browsers/computer_get_mouse_position_response.py b/src/kernel/types/browsers/computer_get_mouse_position_response.py new file mode 100644 index 00000000..53cdc4ad --- /dev/null +++ b/src/kernel/types/browsers/computer_get_mouse_position_response.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from ..._models import BaseModel + +__all__ = ["ComputerGetMousePositionResponse"] + + +class ComputerGetMousePositionResponse(BaseModel): + x: int + """X coordinate of the cursor""" + + y: int + """Y coordinate of the cursor""" diff --git a/tests/api_resources/browsers/test_computer.py b/tests/api_resources/browsers/test_computer.py index 7634b89b..f98f81af 100644 --- a/tests/api_resources/browsers/test_computer.py +++ b/tests/api_resources/browsers/test_computer.py @@ -18,6 +18,7 @@ AsyncStreamedBinaryAPIResponse, ) from kernel.types.browsers import ( + ComputerGetMousePositionResponse, ComputerSetCursorVisibilityResponse, ) @@ -27,6 +28,52 @@ class TestComputer: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_batch(self, client: Kernel) -> None: + computer = client.browsers.computer.batch( + id="id", + actions=[{"type": "click_mouse"}], + ) + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_batch(self, client: Kernel) -> None: + response = client.browsers.computer.with_raw_response.batch( + id="id", + actions=[{"type": "click_mouse"}], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + computer = response.parse() + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_batch(self, client: Kernel) -> None: + with client.browsers.computer.with_streaming_response.batch( + id="id", + actions=[{"type": "click_mouse"}], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + computer = response.parse() + assert computer is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_batch(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.computer.with_raw_response.batch( + id="", + actions=[{"type": "click_mouse"}], + ) + @parametrize @pytest.mark.respx(base_url=base_url) def test_method_capture_screenshot(self, client: Kernel, respx_mock: MockRouter) -> None: @@ -219,6 +266,48 @@ def test_path_params_drag_mouse(self, client: Kernel) -> None: path=[[0, 0], [0, 0]], ) + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_get_mouse_position(self, client: Kernel) -> None: + computer = client.browsers.computer.get_mouse_position( + "id", + ) + assert_matches_type(ComputerGetMousePositionResponse, computer, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_get_mouse_position(self, client: Kernel) -> None: + response = client.browsers.computer.with_raw_response.get_mouse_position( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + computer = response.parse() + assert_matches_type(ComputerGetMousePositionResponse, computer, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_get_mouse_position(self, client: Kernel) -> None: + with client.browsers.computer.with_streaming_response.get_mouse_position( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + computer = response.parse() + assert_matches_type(ComputerGetMousePositionResponse, computer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_get_mouse_position(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.computer.with_raw_response.get_mouse_position( + "", + ) + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_move_mouse(self, client: Kernel) -> None: @@ -508,6 +597,52 @@ class TestAsyncComputer: "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_batch(self, async_client: AsyncKernel) -> None: + computer = await async_client.browsers.computer.batch( + id="id", + actions=[{"type": "click_mouse"}], + ) + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_batch(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.computer.with_raw_response.batch( + id="id", + actions=[{"type": "click_mouse"}], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + computer = await response.parse() + assert computer is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_batch(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.computer.with_streaming_response.batch( + id="id", + actions=[{"type": "click_mouse"}], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + computer = await response.parse() + assert computer is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_batch(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.computer.with_raw_response.batch( + id="", + actions=[{"type": "click_mouse"}], + ) + @parametrize @pytest.mark.respx(base_url=base_url) async def test_method_capture_screenshot(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: @@ -704,6 +839,48 @@ async def test_path_params_drag_mouse(self, async_client: AsyncKernel) -> None: path=[[0, 0], [0, 0]], ) + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_get_mouse_position(self, async_client: AsyncKernel) -> None: + computer = await async_client.browsers.computer.get_mouse_position( + "id", + ) + assert_matches_type(ComputerGetMousePositionResponse, computer, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_get_mouse_position(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.computer.with_raw_response.get_mouse_position( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + computer = await response.parse() + assert_matches_type(ComputerGetMousePositionResponse, computer, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_get_mouse_position(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.computer.with_streaming_response.get_mouse_position( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + computer = await response.parse() + assert_matches_type(ComputerGetMousePositionResponse, computer, 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_mouse_position(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.computer.with_raw_response.get_mouse_position( + "", + ) + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_move_mouse(self, async_client: AsyncKernel) -> None: From f8655dfe283bc6fb8369bd89f1c38e1de0b716ea Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 19:08:08 +0000 Subject: [PATCH 282/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 554e34bb..f81bf992 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.30.0" + ".": "0.31.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 16affc3d..898d5dfd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.30.0" +version = "0.31.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index e08b152b..4d628238 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.30.0" # x-release-please-version +__version__ = "0.31.0" # x-release-please-version From 258f37d0893024a9152c9567bb3c2501e679bc78 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 20:32:30 +0000 Subject: [PATCH 283/448] chore: add Managed Auth API planning doc --- .stats.yml | 8 +- api.md | 43 +- src/kernel/_client.py | 38 + src/kernel/resources/__init__.py | 14 + src/kernel/resources/agents/auth/auth.py | 186 ++-- .../resources/agents/auth/invocations.py | 227 +++-- src/kernel/resources/auth/__init__.py | 33 + src/kernel/resources/auth/auth.py | 102 ++ src/kernel/resources/auth/connections.py | 887 ++++++++++++++++++ src/kernel/resources/credential_providers.py | 121 ++- src/kernel/types/__init__.py | 4 + .../agents/agent_auth_invocation_response.py | 15 +- .../agents/auth/invocation_submit_params.py | 6 +- src/kernel/types/agents/auth_agent.py | 55 +- .../auth_agent_invocation_create_response.py | 9 +- src/kernel/types/agents/auth_create_params.py | 15 + src/kernel/types/agents/discovered_field.py | 6 + src/kernel/types/auth/__init__.py | 12 + .../types/auth/connection_create_params.py | 89 ++ .../types/auth/connection_follow_response.py | 109 +++ .../types/auth/connection_list_params.py | 21 + .../types/auth/connection_login_params.py | 12 + .../types/auth/connection_submit_params.py | 19 + src/kernel/types/auth/login_response.py | 31 + src/kernel/types/auth/managed_auth.py | 181 ++++ .../types/auth/submit_fields_response.py | 12 + src/kernel/types/credential_provider.py | 3 + .../credential_provider_create_params.py | 3 + src/kernel/types/credential_provider_item.py | 29 + ...credential_provider_list_items_response.py | 12 + .../credential_provider_update_params.py | 3 + .../agents/auth/test_invocations.py | 588 ++++++------ tests/api_resources/agents/test_auth.py | 330 ++++--- tests/api_resources/auth/__init__.py | 1 + tests/api_resources/auth/test_connections.py | 715 ++++++++++++++ .../test_credential_providers.py | 95 ++ 36 files changed, 3439 insertions(+), 595 deletions(-) create mode 100644 src/kernel/resources/auth/__init__.py create mode 100644 src/kernel/resources/auth/auth.py create mode 100644 src/kernel/resources/auth/connections.py create mode 100644 src/kernel/types/auth/__init__.py create mode 100644 src/kernel/types/auth/connection_create_params.py create mode 100644 src/kernel/types/auth/connection_follow_response.py create mode 100644 src/kernel/types/auth/connection_list_params.py create mode 100644 src/kernel/types/auth/connection_login_params.py create mode 100644 src/kernel/types/auth/connection_submit_params.py create mode 100644 src/kernel/types/auth/login_response.py create mode 100644 src/kernel/types/auth/managed_auth.py create mode 100644 src/kernel/types/auth/submit_fields_response.py create mode 100644 src/kernel/types/credential_provider_item.py create mode 100644 src/kernel/types/credential_provider_list_items_response.py create mode 100644 tests/api_resources/auth/__init__.py create mode 100644 tests/api_resources/auth/test_connections.py diff --git a/.stats.yml b/.stats.yml index 91fd3b68..29643219 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 100 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-a6d93dc291278035c96add38bb6150ec2b9ba8bbabb4676e3dbbb8444cf3b1e4.yml -openapi_spec_hash: 694bcc56d94fd0ff0d1f7b0fc1dae8ba -config_hash: 62e33cf2ed8fe0b4ceebba63367481ad +configured_endpoints: 108 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-3fbe762c99e8a120c426ac22bc1fa257c9127d631b12a38a6440a37f52935543.yml +openapi_spec_hash: 5a190df210ed90b20a71c5061ff43917 +config_hash: 38c9b3b355025daf9bb643040e4af94e diff --git a/api.md b/api.md index d55fecff..cb50de40 100644 --- a/api.md +++ b/api.md @@ -234,6 +234,34 @@ Methods: - client.profiles.delete(id_or_name) -> None - client.profiles.download(id_or_name) -> BinaryAPIResponse +# Auth + +## Connections + +Types: + +```python +from kernel.types.auth import ( + LoginRequest, + LoginResponse, + ManagedAuth, + ManagedAuthCreateRequest, + SubmitFieldsRequest, + SubmitFieldsResponse, + ConnectionFollowResponse, +) +``` + +Methods: + +- client.auth.connections.create(\*\*params) -> ManagedAuth +- client.auth.connections.retrieve(id) -> ManagedAuth +- client.auth.connections.list(\*\*params) -> SyncOffsetPagination[ManagedAuth] +- client.auth.connections.delete(id) -> None +- client.auth.connections.follow(id) -> ConnectionFollowResponse +- client.auth.connections.login(id, \*\*params) -> LoginResponse +- client.auth.connections.submit(id, \*\*params) -> SubmitFieldsResponse + # Proxies Types: @@ -360,17 +388,20 @@ Types: from kernel.types import ( CreateCredentialProviderRequest, CredentialProvider, + CredentialProviderItem, CredentialProviderTestResult, UpdateCredentialProviderRequest, CredentialProviderListResponse, + CredentialProviderListItemsResponse, ) ``` Methods: -- client.credential_providers.create(\*\*params) -> CredentialProvider -- client.credential_providers.retrieve(id) -> CredentialProvider -- client.credential_providers.update(id, \*\*params) -> CredentialProvider -- client.credential_providers.list() -> CredentialProviderListResponse -- client.credential_providers.delete(id) -> None -- client.credential_providers.test(id) -> CredentialProviderTestResult +- client.credential_providers.create(\*\*params) -> CredentialProvider +- client.credential_providers.retrieve(id) -> CredentialProvider +- client.credential_providers.update(id, \*\*params) -> CredentialProvider +- client.credential_providers.list() -> CredentialProviderListResponse +- client.credential_providers.delete(id) -> None +- client.credential_providers.list_items(id) -> CredentialProviderListItemsResponse +- client.credential_providers.test(id) -> CredentialProviderTestResult diff --git a/src/kernel/_client.py b/src/kernel/_client.py index daf57998..07c3b682 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -33,6 +33,7 @@ if TYPE_CHECKING: from .resources import ( apps, + auth, agents, proxies, browsers, @@ -47,6 +48,7 @@ from .resources.apps import AppsResource, AsyncAppsResource from .resources.proxies import ProxiesResource, AsyncProxiesResource from .resources.profiles import ProfilesResource, AsyncProfilesResource + from .resources.auth.auth import AuthResource, AsyncAuthResource from .resources.extensions import ExtensionsResource, AsyncExtensionsResource from .resources.credentials import CredentialsResource, AsyncCredentialsResource from .resources.deployments import DeploymentsResource, AsyncDeploymentsResource @@ -183,6 +185,12 @@ def profiles(self) -> ProfilesResource: return ProfilesResource(self) + @cached_property + def auth(self) -> AuthResource: + from .resources.auth import AuthResource + + return AuthResource(self) + @cached_property def proxies(self) -> ProxiesResource: from .resources.proxies import ProxiesResource @@ -443,6 +451,12 @@ def profiles(self) -> AsyncProfilesResource: return AsyncProfilesResource(self) + @cached_property + def auth(self) -> AsyncAuthResource: + from .resources.auth import AsyncAuthResource + + return AsyncAuthResource(self) + @cached_property def proxies(self) -> AsyncProxiesResource: from .resources.proxies import AsyncProxiesResource @@ -630,6 +644,12 @@ def profiles(self) -> profiles.ProfilesResourceWithRawResponse: return ProfilesResourceWithRawResponse(self._client.profiles) + @cached_property + def auth(self) -> auth.AuthResourceWithRawResponse: + from .resources.auth import AuthResourceWithRawResponse + + return AuthResourceWithRawResponse(self._client.auth) + @cached_property def proxies(self) -> proxies.ProxiesResourceWithRawResponse: from .resources.proxies import ProxiesResourceWithRawResponse @@ -703,6 +723,12 @@ def profiles(self) -> profiles.AsyncProfilesResourceWithRawResponse: return AsyncProfilesResourceWithRawResponse(self._client.profiles) + @cached_property + def auth(self) -> auth.AsyncAuthResourceWithRawResponse: + from .resources.auth import AsyncAuthResourceWithRawResponse + + return AsyncAuthResourceWithRawResponse(self._client.auth) + @cached_property def proxies(self) -> proxies.AsyncProxiesResourceWithRawResponse: from .resources.proxies import AsyncProxiesResourceWithRawResponse @@ -776,6 +802,12 @@ def profiles(self) -> profiles.ProfilesResourceWithStreamingResponse: return ProfilesResourceWithStreamingResponse(self._client.profiles) + @cached_property + def auth(self) -> auth.AuthResourceWithStreamingResponse: + from .resources.auth import AuthResourceWithStreamingResponse + + return AuthResourceWithStreamingResponse(self._client.auth) + @cached_property def proxies(self) -> proxies.ProxiesResourceWithStreamingResponse: from .resources.proxies import ProxiesResourceWithStreamingResponse @@ -849,6 +881,12 @@ def profiles(self) -> profiles.AsyncProfilesResourceWithStreamingResponse: return AsyncProfilesResourceWithStreamingResponse(self._client.profiles) + @cached_property + def auth(self) -> auth.AsyncAuthResourceWithStreamingResponse: + from .resources.auth import AsyncAuthResourceWithStreamingResponse + + return AsyncAuthResourceWithStreamingResponse(self._client.auth) + @cached_property def proxies(self) -> proxies.AsyncProxiesResourceWithStreamingResponse: from .resources.proxies import AsyncProxiesResourceWithStreamingResponse diff --git a/src/kernel/resources/__init__.py b/src/kernel/resources/__init__.py index 50db63bb..31a325b2 100644 --- a/src/kernel/resources/__init__.py +++ b/src/kernel/resources/__init__.py @@ -8,6 +8,14 @@ AppsResourceWithStreamingResponse, AsyncAppsResourceWithStreamingResponse, ) +from .auth import ( + AuthResource, + AsyncAuthResource, + AuthResourceWithRawResponse, + AsyncAuthResourceWithRawResponse, + AuthResourceWithStreamingResponse, + AsyncAuthResourceWithStreamingResponse, +) from .agents import ( AgentsResource, AsyncAgentsResource, @@ -120,6 +128,12 @@ "AsyncProfilesResourceWithRawResponse", "ProfilesResourceWithStreamingResponse", "AsyncProfilesResourceWithStreamingResponse", + "AuthResource", + "AsyncAuthResource", + "AuthResourceWithRawResponse", + "AsyncAuthResourceWithRawResponse", + "AuthResourceWithStreamingResponse", + "AsyncAuthResourceWithStreamingResponse", "ProxiesResource", "AsyncProxiesResource", "ProxiesResourceWithRawResponse", diff --git a/src/kernel/resources/agents/auth/auth.py b/src/kernel/resources/agents/auth/auth.py index 4a541f73..05e83c50 100644 --- a/src/kernel/resources/agents/auth/auth.py +++ b/src/kernel/resources/agents/auth/auth.py @@ -2,6 +2,8 @@ from __future__ import annotations +import typing_extensions + import httpx from ...._types import Body, Omit, Query, Headers, NoneType, NotGiven, SequenceNotStr, omit, not_given @@ -54,6 +56,7 @@ def with_streaming_response(self) -> AuthResourceWithStreamingResponse: """ return AuthResourceWithStreamingResponse(self) + @typing_extensions.deprecated("deprecated") def create( self, *, @@ -71,10 +74,11 @@ def create( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AuthAgent: """ - Creates a new auth agent for the specified domain and profile combination, or - returns an existing one if it already exists. This is idempotent - calling with - the same domain and profile will return the same agent. Does NOT start an - invocation - use POST /agents/auth/invocations to start an auth flow. + **Deprecated: Use POST /auth/connections instead.** Creates a new auth agent for + the specified domain and profile combination, or returns an existing one if it + already exists. This is idempotent - calling with the same domain and profile + will return the same agent. Does NOT start an invocation - use POST + /agents/auth/invocations to start an auth flow. Args: domain: Domain for authentication @@ -85,6 +89,21 @@ def create( (besides the primary domain). Useful when login pages redirect to different domains. + The following SSO/OAuth provider domains are automatically allowed by default + and do not need to be specified: + + - Google: accounts.google.com + - Microsoft/Azure AD: login.microsoftonline.com, login.live.com + - Okta: _.okta.com, _.oktapreview.com + - Auth0: _.auth0.com, _.us.auth0.com, _.eu.auth0.com, _.au.auth0.com + - Apple: appleid.apple.com + - GitHub: github.com + - Facebook/Meta: www.facebook.com + - LinkedIn: www.linkedin.com + - Amazon Cognito: \\**.amazoncognito.com + - OneLogin: \\**.onelogin.com + - Ping Identity: _.pingone.com, _.pingidentity.com + credential_name: Optional name of an existing credential to use for this auth agent. If provided, the credential will be linked to the agent and its values will be used to auto-fill the login form on invocation. @@ -121,6 +140,7 @@ def create( cast_to=AuthAgent, ) + @typing_extensions.deprecated("deprecated") def retrieve( self, id: str, @@ -132,10 +152,9 @@ def retrieve( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AuthAgent: - """Retrieve an auth agent by its ID. - - Returns the current authentication status of - the managed profile. + """ + **Deprecated: Use GET /auth/connections/{id} instead.** Retrieve an auth agent + by its ID. Returns the current authentication status of the managed profile. Args: extra_headers: Send extra headers @@ -156,6 +175,7 @@ def retrieve( cast_to=AuthAgent, ) + @typing_extensions.deprecated("deprecated") def list( self, *, @@ -171,7 +191,8 @@ def list( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SyncOffsetPagination[AuthAgent]: """ - List auth agents with optional filters for profile_name and domain. + **Deprecated: Use GET /auth/connections instead.** List auth agents with + optional filters for profile_name and domain. Args: domain: Filter by domain @@ -211,6 +232,7 @@ def list( model=AuthAgent, ) + @typing_extensions.deprecated("deprecated") def delete( self, id: str, @@ -222,9 +244,9 @@ def delete( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: - """Deletes an auth agent and terminates its workflow. - - This will: + """ + **Deprecated: Use DELETE /auth/connections/{id} instead.** Deletes an auth agent + and terminates its workflow. This will: - Soft delete the auth agent record - Gracefully terminate the agent's Temporal workflow @@ -275,6 +297,7 @@ def with_streaming_response(self) -> AsyncAuthResourceWithStreamingResponse: """ return AsyncAuthResourceWithStreamingResponse(self) + @typing_extensions.deprecated("deprecated") async def create( self, *, @@ -292,10 +315,11 @@ async def create( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AuthAgent: """ - Creates a new auth agent for the specified domain and profile combination, or - returns an existing one if it already exists. This is idempotent - calling with - the same domain and profile will return the same agent. Does NOT start an - invocation - use POST /agents/auth/invocations to start an auth flow. + **Deprecated: Use POST /auth/connections instead.** Creates a new auth agent for + the specified domain and profile combination, or returns an existing one if it + already exists. This is idempotent - calling with the same domain and profile + will return the same agent. Does NOT start an invocation - use POST + /agents/auth/invocations to start an auth flow. Args: domain: Domain for authentication @@ -306,6 +330,21 @@ async def create( (besides the primary domain). Useful when login pages redirect to different domains. + The following SSO/OAuth provider domains are automatically allowed by default + and do not need to be specified: + + - Google: accounts.google.com + - Microsoft/Azure AD: login.microsoftonline.com, login.live.com + - Okta: _.okta.com, _.oktapreview.com + - Auth0: _.auth0.com, _.us.auth0.com, _.eu.auth0.com, _.au.auth0.com + - Apple: appleid.apple.com + - GitHub: github.com + - Facebook/Meta: www.facebook.com + - LinkedIn: www.linkedin.com + - Amazon Cognito: \\**.amazoncognito.com + - OneLogin: \\**.onelogin.com + - Ping Identity: _.pingone.com, _.pingidentity.com + credential_name: Optional name of an existing credential to use for this auth agent. If provided, the credential will be linked to the agent and its values will be used to auto-fill the login form on invocation. @@ -342,6 +381,7 @@ async def create( cast_to=AuthAgent, ) + @typing_extensions.deprecated("deprecated") async def retrieve( self, id: str, @@ -353,10 +393,9 @@ async def retrieve( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AuthAgent: - """Retrieve an auth agent by its ID. - - Returns the current authentication status of - the managed profile. + """ + **Deprecated: Use GET /auth/connections/{id} instead.** Retrieve an auth agent + by its ID. Returns the current authentication status of the managed profile. Args: extra_headers: Send extra headers @@ -377,6 +416,7 @@ async def retrieve( cast_to=AuthAgent, ) + @typing_extensions.deprecated("deprecated") def list( self, *, @@ -392,7 +432,8 @@ def list( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncPaginator[AuthAgent, AsyncOffsetPagination[AuthAgent]]: """ - List auth agents with optional filters for profile_name and domain. + **Deprecated: Use GET /auth/connections instead.** List auth agents with + optional filters for profile_name and domain. Args: domain: Filter by domain @@ -432,6 +473,7 @@ def list( model=AuthAgent, ) + @typing_extensions.deprecated("deprecated") async def delete( self, id: str, @@ -443,9 +485,9 @@ async def delete( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: - """Deletes an auth agent and terminates its workflow. - - This will: + """ + **Deprecated: Use DELETE /auth/connections/{id} instead.** Deletes an auth agent + and terminates its workflow. This will: - Soft delete the auth agent record - Gracefully terminate the agent's Temporal workflow @@ -476,17 +518,25 @@ class AuthResourceWithRawResponse: def __init__(self, auth: AuthResource) -> None: self._auth = auth - self.create = to_raw_response_wrapper( - auth.create, + self.create = ( # pyright: ignore[reportDeprecated] + to_raw_response_wrapper( + auth.create, # pyright: ignore[reportDeprecated], + ) ) - self.retrieve = to_raw_response_wrapper( - auth.retrieve, + self.retrieve = ( # pyright: ignore[reportDeprecated] + to_raw_response_wrapper( + auth.retrieve, # pyright: ignore[reportDeprecated], + ) ) - self.list = to_raw_response_wrapper( - auth.list, + self.list = ( # pyright: ignore[reportDeprecated] + to_raw_response_wrapper( + auth.list, # pyright: ignore[reportDeprecated], + ) ) - self.delete = to_raw_response_wrapper( - auth.delete, + self.delete = ( # pyright: ignore[reportDeprecated] + to_raw_response_wrapper( + auth.delete, # pyright: ignore[reportDeprecated], + ) ) @cached_property @@ -498,17 +548,25 @@ class AsyncAuthResourceWithRawResponse: def __init__(self, auth: AsyncAuthResource) -> None: self._auth = auth - self.create = async_to_raw_response_wrapper( - auth.create, + self.create = ( # pyright: ignore[reportDeprecated] + async_to_raw_response_wrapper( + auth.create, # pyright: ignore[reportDeprecated], + ) ) - self.retrieve = async_to_raw_response_wrapper( - auth.retrieve, + self.retrieve = ( # pyright: ignore[reportDeprecated] + async_to_raw_response_wrapper( + auth.retrieve, # pyright: ignore[reportDeprecated], + ) ) - self.list = async_to_raw_response_wrapper( - auth.list, + self.list = ( # pyright: ignore[reportDeprecated] + async_to_raw_response_wrapper( + auth.list, # pyright: ignore[reportDeprecated], + ) ) - self.delete = async_to_raw_response_wrapper( - auth.delete, + self.delete = ( # pyright: ignore[reportDeprecated] + async_to_raw_response_wrapper( + auth.delete, # pyright: ignore[reportDeprecated], + ) ) @cached_property @@ -520,17 +578,25 @@ class AuthResourceWithStreamingResponse: def __init__(self, auth: AuthResource) -> None: self._auth = auth - self.create = to_streamed_response_wrapper( - auth.create, + self.create = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + auth.create, # pyright: ignore[reportDeprecated], + ) ) - self.retrieve = to_streamed_response_wrapper( - auth.retrieve, + self.retrieve = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + auth.retrieve, # pyright: ignore[reportDeprecated], + ) ) - self.list = to_streamed_response_wrapper( - auth.list, + self.list = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + auth.list, # pyright: ignore[reportDeprecated], + ) ) - self.delete = to_streamed_response_wrapper( - auth.delete, + self.delete = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + auth.delete, # pyright: ignore[reportDeprecated], + ) ) @cached_property @@ -542,17 +608,25 @@ class AsyncAuthResourceWithStreamingResponse: def __init__(self, auth: AsyncAuthResource) -> None: self._auth = auth - self.create = async_to_streamed_response_wrapper( - auth.create, + self.create = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + auth.create, # pyright: ignore[reportDeprecated], + ) ) - self.retrieve = async_to_streamed_response_wrapper( - auth.retrieve, + self.retrieve = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + auth.retrieve, # pyright: ignore[reportDeprecated], + ) ) - self.list = async_to_streamed_response_wrapper( - auth.list, + self.list = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + auth.list, # pyright: ignore[reportDeprecated], + ) ) - self.delete = async_to_streamed_response_wrapper( - auth.delete, + self.delete = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + auth.delete, # pyright: ignore[reportDeprecated], + ) ) @cached_property diff --git a/src/kernel/resources/agents/auth/invocations.py b/src/kernel/resources/agents/auth/invocations.py index fcd5e3a6..617afddd 100644 --- a/src/kernel/resources/agents/auth/invocations.py +++ b/src/kernel/resources/agents/auth/invocations.py @@ -2,6 +2,7 @@ from __future__ import annotations +import typing_extensions from typing import Dict from typing_extensions import Literal, overload @@ -47,6 +48,7 @@ def with_streaming_response(self) -> InvocationsResourceWithStreamingResponse: """ return InvocationsResourceWithStreamingResponse(self) + @typing_extensions.deprecated("deprecated") def create( self, *, @@ -59,11 +61,10 @@ def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AuthAgentInvocationCreateResponse: - """Creates a new authentication invocation for the specified auth agent. - - This - starts the auth flow and returns a hosted URL for the user to complete - authentication. + """ + **Deprecated: Use POST /auth/connections/{id}/login instead.** Creates a new + authentication invocation for the specified auth agent. This starts the auth + flow and returns a hosted URL for the user to complete authentication. Args: auth_agent_id: ID of the auth agent to create an invocation for @@ -95,6 +96,7 @@ def create( cast_to=AuthAgentInvocationCreateResponse, ) + @typing_extensions.deprecated("deprecated") def retrieve( self, invocation_id: str, @@ -106,10 +108,10 @@ def retrieve( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AgentAuthInvocationResponse: - """Returns invocation details including status, app_name, and domain. - - Supports both - API key and JWT (from exchange endpoint) authentication. + """ + **Deprecated: Use GET /auth/connections/{id} instead.** Returns invocation + details including status, app_name, and domain. Supports both API key and JWT + (from exchange endpoint) authentication. Args: extra_headers: Send extra headers @@ -130,6 +132,7 @@ def retrieve( cast_to=AgentAuthInvocationResponse, ) + @typing_extensions.deprecated("deprecated") def exchange( self, invocation_id: str, @@ -142,10 +145,10 @@ def exchange( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> InvocationExchangeResponse: - """Validates the handoff code and returns a JWT token for subsequent requests. - - No - authentication required (the handoff code serves as the credential). + """ + **Deprecated: Use POST /auth/connections/{id}/exchange instead.** Validates the + handoff code and returns a JWT token for subsequent requests. No authentication + required (the handoff code serves as the credential). Args: code: Handoff code from start endpoint @@ -169,6 +172,7 @@ def exchange( cast_to=InvocationExchangeResponse, ) + @typing_extensions.deprecated("deprecated") @overload def submit( self, @@ -182,11 +186,10 @@ def submit( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AgentAuthSubmitResponse: - """Submits field values for the discovered login form. - - Returns immediately after - submission is accepted. Poll the invocation endpoint to track progress and get - results. + """ + **Deprecated: Use POST /auth/connections/{id}/submit instead.** Submits field + values for the discovered login form. Returns immediately after submission is + accepted. Poll the invocation endpoint to track progress and get results. Args: field_values: Values for the discovered login fields @@ -201,6 +204,7 @@ def submit( """ ... + @typing_extensions.deprecated("deprecated") @overload def submit( self, @@ -214,11 +218,10 @@ def submit( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AgentAuthSubmitResponse: - """Submits field values for the discovered login form. - - Returns immediately after - submission is accepted. Poll the invocation endpoint to track progress and get - results. + """ + **Deprecated: Use POST /auth/connections/{id}/submit instead.** Submits field + values for the discovered login form. Returns immediately after submission is + accepted. Poll the invocation endpoint to track progress and get results. Args: sso_button: Selector of SSO button to click @@ -233,12 +236,13 @@ def submit( """ ... + @typing_extensions.deprecated("deprecated") @overload def submit( self, invocation_id: str, *, - selected_mfa_type: Literal["sms", "call", "email", "totp", "push", "security_key"], + selected_mfa_type: Literal["sms", "call", "email", "totp", "push", "password"], # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -246,14 +250,13 @@ def submit( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AgentAuthSubmitResponse: - """Submits field values for the discovered login form. - - Returns immediately after - submission is accepted. Poll the invocation endpoint to track progress and get - results. + """ + **Deprecated: Use POST /auth/connections/{id}/submit instead.** Submits field + values for the discovered login form. Returns immediately after submission is + accepted. Poll the invocation endpoint to track progress and get results. Args: - selected_mfa_type: The MFA delivery method type + selected_mfa_type: The MFA delivery method type (includes password for auth method selection pages) extra_headers: Send extra headers @@ -265,6 +268,7 @@ def submit( """ ... + @typing_extensions.deprecated("deprecated") @required_args(["field_values"], ["sso_button"], ["selected_mfa_type"]) def submit( self, @@ -272,7 +276,7 @@ def submit( *, field_values: Dict[str, str] | Omit = omit, sso_button: str | Omit = omit, - selected_mfa_type: Literal["sms", "call", "email", "totp", "push", "security_key"] | Omit = omit, + selected_mfa_type: Literal["sms", "call", "email", "totp", "push", "password"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -319,6 +323,7 @@ def with_streaming_response(self) -> AsyncInvocationsResourceWithStreamingRespon """ return AsyncInvocationsResourceWithStreamingResponse(self) + @typing_extensions.deprecated("deprecated") async def create( self, *, @@ -331,11 +336,10 @@ async def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AuthAgentInvocationCreateResponse: - """Creates a new authentication invocation for the specified auth agent. - - This - starts the auth flow and returns a hosted URL for the user to complete - authentication. + """ + **Deprecated: Use POST /auth/connections/{id}/login instead.** Creates a new + authentication invocation for the specified auth agent. This starts the auth + flow and returns a hosted URL for the user to complete authentication. Args: auth_agent_id: ID of the auth agent to create an invocation for @@ -367,6 +371,7 @@ async def create( cast_to=AuthAgentInvocationCreateResponse, ) + @typing_extensions.deprecated("deprecated") async def retrieve( self, invocation_id: str, @@ -378,10 +383,10 @@ async def retrieve( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AgentAuthInvocationResponse: - """Returns invocation details including status, app_name, and domain. - - Supports both - API key and JWT (from exchange endpoint) authentication. + """ + **Deprecated: Use GET /auth/connections/{id} instead.** Returns invocation + details including status, app_name, and domain. Supports both API key and JWT + (from exchange endpoint) authentication. Args: extra_headers: Send extra headers @@ -402,6 +407,7 @@ async def retrieve( cast_to=AgentAuthInvocationResponse, ) + @typing_extensions.deprecated("deprecated") async def exchange( self, invocation_id: str, @@ -414,10 +420,10 @@ async def exchange( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> InvocationExchangeResponse: - """Validates the handoff code and returns a JWT token for subsequent requests. - - No - authentication required (the handoff code serves as the credential). + """ + **Deprecated: Use POST /auth/connections/{id}/exchange instead.** Validates the + handoff code and returns a JWT token for subsequent requests. No authentication + required (the handoff code serves as the credential). Args: code: Handoff code from start endpoint @@ -441,6 +447,7 @@ async def exchange( cast_to=InvocationExchangeResponse, ) + @typing_extensions.deprecated("deprecated") @overload async def submit( self, @@ -454,11 +461,10 @@ async def submit( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AgentAuthSubmitResponse: - """Submits field values for the discovered login form. - - Returns immediately after - submission is accepted. Poll the invocation endpoint to track progress and get - results. + """ + **Deprecated: Use POST /auth/connections/{id}/submit instead.** Submits field + values for the discovered login form. Returns immediately after submission is + accepted. Poll the invocation endpoint to track progress and get results. Args: field_values: Values for the discovered login fields @@ -473,6 +479,7 @@ async def submit( """ ... + @typing_extensions.deprecated("deprecated") @overload async def submit( self, @@ -486,11 +493,10 @@ async def submit( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AgentAuthSubmitResponse: - """Submits field values for the discovered login form. - - Returns immediately after - submission is accepted. Poll the invocation endpoint to track progress and get - results. + """ + **Deprecated: Use POST /auth/connections/{id}/submit instead.** Submits field + values for the discovered login form. Returns immediately after submission is + accepted. Poll the invocation endpoint to track progress and get results. Args: sso_button: Selector of SSO button to click @@ -505,12 +511,13 @@ async def submit( """ ... + @typing_extensions.deprecated("deprecated") @overload async def submit( self, invocation_id: str, *, - selected_mfa_type: Literal["sms", "call", "email", "totp", "push", "security_key"], + selected_mfa_type: Literal["sms", "call", "email", "totp", "push", "password"], # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -518,14 +525,13 @@ async def submit( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AgentAuthSubmitResponse: - """Submits field values for the discovered login form. - - Returns immediately after - submission is accepted. Poll the invocation endpoint to track progress and get - results. + """ + **Deprecated: Use POST /auth/connections/{id}/submit instead.** Submits field + values for the discovered login form. Returns immediately after submission is + accepted. Poll the invocation endpoint to track progress and get results. Args: - selected_mfa_type: The MFA delivery method type + selected_mfa_type: The MFA delivery method type (includes password for auth method selection pages) extra_headers: Send extra headers @@ -537,6 +543,7 @@ async def submit( """ ... + @typing_extensions.deprecated("deprecated") @required_args(["field_values"], ["sso_button"], ["selected_mfa_type"]) async def submit( self, @@ -544,7 +551,7 @@ async def submit( *, field_values: Dict[str, str] | Omit = omit, sso_button: str | Omit = omit, - selected_mfa_type: Literal["sms", "call", "email", "totp", "push", "security_key"] | Omit = omit, + selected_mfa_type: Literal["sms", "call", "email", "totp", "push", "password"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -575,17 +582,25 @@ class InvocationsResourceWithRawResponse: def __init__(self, invocations: InvocationsResource) -> None: self._invocations = invocations - self.create = to_raw_response_wrapper( - invocations.create, + self.create = ( # pyright: ignore[reportDeprecated] + to_raw_response_wrapper( + invocations.create, # pyright: ignore[reportDeprecated], + ) ) - self.retrieve = to_raw_response_wrapper( - invocations.retrieve, + self.retrieve = ( # pyright: ignore[reportDeprecated] + to_raw_response_wrapper( + invocations.retrieve, # pyright: ignore[reportDeprecated], + ) ) - self.exchange = to_raw_response_wrapper( - invocations.exchange, + self.exchange = ( # pyright: ignore[reportDeprecated] + to_raw_response_wrapper( + invocations.exchange, # pyright: ignore[reportDeprecated], + ) ) - self.submit = to_raw_response_wrapper( - invocations.submit, + self.submit = ( # pyright: ignore[reportDeprecated] + to_raw_response_wrapper( + invocations.submit, # pyright: ignore[reportDeprecated], + ) ) @@ -593,17 +608,25 @@ class AsyncInvocationsResourceWithRawResponse: def __init__(self, invocations: AsyncInvocationsResource) -> None: self._invocations = invocations - self.create = async_to_raw_response_wrapper( - invocations.create, + self.create = ( # pyright: ignore[reportDeprecated] + async_to_raw_response_wrapper( + invocations.create, # pyright: ignore[reportDeprecated], + ) ) - self.retrieve = async_to_raw_response_wrapper( - invocations.retrieve, + self.retrieve = ( # pyright: ignore[reportDeprecated] + async_to_raw_response_wrapper( + invocations.retrieve, # pyright: ignore[reportDeprecated], + ) ) - self.exchange = async_to_raw_response_wrapper( - invocations.exchange, + self.exchange = ( # pyright: ignore[reportDeprecated] + async_to_raw_response_wrapper( + invocations.exchange, # pyright: ignore[reportDeprecated], + ) ) - self.submit = async_to_raw_response_wrapper( - invocations.submit, + self.submit = ( # pyright: ignore[reportDeprecated] + async_to_raw_response_wrapper( + invocations.submit, # pyright: ignore[reportDeprecated], + ) ) @@ -611,17 +634,25 @@ class InvocationsResourceWithStreamingResponse: def __init__(self, invocations: InvocationsResource) -> None: self._invocations = invocations - self.create = to_streamed_response_wrapper( - invocations.create, + self.create = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + invocations.create, # pyright: ignore[reportDeprecated], + ) ) - self.retrieve = to_streamed_response_wrapper( - invocations.retrieve, + self.retrieve = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + invocations.retrieve, # pyright: ignore[reportDeprecated], + ) ) - self.exchange = to_streamed_response_wrapper( - invocations.exchange, + self.exchange = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + invocations.exchange, # pyright: ignore[reportDeprecated], + ) ) - self.submit = to_streamed_response_wrapper( - invocations.submit, + self.submit = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + invocations.submit, # pyright: ignore[reportDeprecated], + ) ) @@ -629,15 +660,23 @@ class AsyncInvocationsResourceWithStreamingResponse: def __init__(self, invocations: AsyncInvocationsResource) -> None: self._invocations = invocations - self.create = async_to_streamed_response_wrapper( - invocations.create, + self.create = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + invocations.create, # pyright: ignore[reportDeprecated], + ) ) - self.retrieve = async_to_streamed_response_wrapper( - invocations.retrieve, + self.retrieve = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + invocations.retrieve, # pyright: ignore[reportDeprecated], + ) ) - self.exchange = async_to_streamed_response_wrapper( - invocations.exchange, + self.exchange = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + invocations.exchange, # pyright: ignore[reportDeprecated], + ) ) - self.submit = async_to_streamed_response_wrapper( - invocations.submit, + self.submit = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + invocations.submit, # pyright: ignore[reportDeprecated], + ) ) diff --git a/src/kernel/resources/auth/__init__.py b/src/kernel/resources/auth/__init__.py new file mode 100644 index 00000000..a863677f --- /dev/null +++ b/src/kernel/resources/auth/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .auth import ( + AuthResource, + AsyncAuthResource, + AuthResourceWithRawResponse, + AsyncAuthResourceWithRawResponse, + AuthResourceWithStreamingResponse, + AsyncAuthResourceWithStreamingResponse, +) +from .connections import ( + ConnectionsResource, + AsyncConnectionsResource, + ConnectionsResourceWithRawResponse, + AsyncConnectionsResourceWithRawResponse, + ConnectionsResourceWithStreamingResponse, + AsyncConnectionsResourceWithStreamingResponse, +) + +__all__ = [ + "ConnectionsResource", + "AsyncConnectionsResource", + "ConnectionsResourceWithRawResponse", + "AsyncConnectionsResourceWithRawResponse", + "ConnectionsResourceWithStreamingResponse", + "AsyncConnectionsResourceWithStreamingResponse", + "AuthResource", + "AsyncAuthResource", + "AuthResourceWithRawResponse", + "AsyncAuthResourceWithRawResponse", + "AuthResourceWithStreamingResponse", + "AsyncAuthResourceWithStreamingResponse", +] diff --git a/src/kernel/resources/auth/auth.py b/src/kernel/resources/auth/auth.py new file mode 100644 index 00000000..b5980e6b --- /dev/null +++ b/src/kernel/resources/auth/auth.py @@ -0,0 +1,102 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from .connections import ( + ConnectionsResource, + AsyncConnectionsResource, + ConnectionsResourceWithRawResponse, + AsyncConnectionsResourceWithRawResponse, + ConnectionsResourceWithStreamingResponse, + AsyncConnectionsResourceWithStreamingResponse, +) + +__all__ = ["AuthResource", "AsyncAuthResource"] + + +class AuthResource(SyncAPIResource): + @cached_property + def connections(self) -> ConnectionsResource: + return ConnectionsResource(self._client) + + @cached_property + def with_raw_response(self) -> AuthResourceWithRawResponse: + """ + 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/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AuthResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AuthResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response + """ + return AuthResourceWithStreamingResponse(self) + + +class AsyncAuthResource(AsyncAPIResource): + @cached_property + def connections(self) -> AsyncConnectionsResource: + return AsyncConnectionsResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncAuthResourceWithRawResponse: + """ + 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/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncAuthResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAuthResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response + """ + return AsyncAuthResourceWithStreamingResponse(self) + + +class AuthResourceWithRawResponse: + def __init__(self, auth: AuthResource) -> None: + self._auth = auth + + @cached_property + def connections(self) -> ConnectionsResourceWithRawResponse: + return ConnectionsResourceWithRawResponse(self._auth.connections) + + +class AsyncAuthResourceWithRawResponse: + def __init__(self, auth: AsyncAuthResource) -> None: + self._auth = auth + + @cached_property + def connections(self) -> AsyncConnectionsResourceWithRawResponse: + return AsyncConnectionsResourceWithRawResponse(self._auth.connections) + + +class AuthResourceWithStreamingResponse: + def __init__(self, auth: AuthResource) -> None: + self._auth = auth + + @cached_property + def connections(self) -> ConnectionsResourceWithStreamingResponse: + return ConnectionsResourceWithStreamingResponse(self._auth.connections) + + +class AsyncAuthResourceWithStreamingResponse: + def __init__(self, auth: AsyncAuthResource) -> None: + self._auth = auth + + @cached_property + def connections(self) -> AsyncConnectionsResourceWithStreamingResponse: + return AsyncConnectionsResourceWithStreamingResponse(self._auth.connections) diff --git a/src/kernel/resources/auth/connections.py b/src/kernel/resources/auth/connections.py new file mode 100644 index 00000000..0ae31538 --- /dev/null +++ b/src/kernel/resources/auth/connections.py @@ -0,0 +1,887 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Any, Dict, cast + +import httpx + +from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, SequenceNotStr, omit, not_given +from ..._utils import maybe_transform, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..._streaming import Stream, AsyncStream +from ...pagination import SyncOffsetPagination, AsyncOffsetPagination +from ...types.auth import ( + connection_list_params, + connection_login_params, + connection_create_params, + connection_submit_params, +) +from ..._base_client import AsyncPaginator, make_request_options +from ...types.auth.managed_auth import ManagedAuth +from ...types.auth.login_response import LoginResponse +from ...types.auth.submit_fields_response import SubmitFieldsResponse +from ...types.auth.connection_follow_response import ConnectionFollowResponse + +__all__ = ["ConnectionsResource", "AsyncConnectionsResource"] + + +class ConnectionsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> ConnectionsResourceWithRawResponse: + """ + 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/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return ConnectionsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ConnectionsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response + """ + return ConnectionsResourceWithStreamingResponse(self) + + def create( + self, + *, + domain: str, + profile_name: str, + allowed_domains: SequenceNotStr[str] | Omit = omit, + credential: connection_create_params.Credential | Omit = omit, + health_check_interval: int | Omit = omit, + login_url: str | Omit = omit, + proxy: connection_create_params.Proxy | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ManagedAuth: + """Creates managed authentication for a profile and domain combination. + + Returns 409 + Conflict if managed auth already exists for the given profile and domain. + + Args: + domain: Domain for authentication + + profile_name: Name of the profile to manage authentication for + + allowed_domains: Additional domains valid for this auth flow (besides the primary domain). Useful + when login pages redirect to different domains. + + The following SSO/OAuth provider domains are automatically allowed by default + and do not need to be specified: + + - Google: accounts.google.com + - Microsoft/Azure AD: login.microsoftonline.com, login.live.com + - Okta: _.okta.com, _.oktapreview.com + - Auth0: _.auth0.com, _.us.auth0.com, _.eu.auth0.com, _.au.auth0.com + - Apple: appleid.apple.com + - GitHub: github.com + - Facebook/Meta: www.facebook.com + - LinkedIn: www.linkedin.com + - Amazon Cognito: \\**.amazoncognito.com + - OneLogin: \\**.onelogin.com + - Ping Identity: _.pingone.com, _.pingidentity.com + + credential: + Reference to credentials for managed auth. Use one of: + + - { name } for Kernel credentials + - { provider, path } for external provider item + - { provider, auto: true } for external provider domain lookup + + health_check_interval: Interval in seconds between automatic health checks. When set, the system + periodically verifies the authentication status and triggers re-authentication + if needed. Must be between 300 (5 minutes) and 86400 (24 hours). Default is 3600 + (1 hour). + + login_url: Optional login page URL to skip discovery + + proxy: Optional proxy configuration + + 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( + "/auth/connections", + body=maybe_transform( + { + "domain": domain, + "profile_name": profile_name, + "allowed_domains": allowed_domains, + "credential": credential, + "health_check_interval": health_check_interval, + "login_url": login_url, + "proxy": proxy, + }, + connection_create_params.ConnectionCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ManagedAuth, + ) + + def retrieve( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ManagedAuth: + """Retrieve managed auth by its ID. + + Includes current flow state if a login is in + progress. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/auth/connections/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ManagedAuth, + ) + + def list( + self, + *, + domain: str | Omit = omit, + limit: int | Omit = omit, + offset: int | Omit = omit, + profile_name: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SyncOffsetPagination[ManagedAuth]: + """ + List managed auths with optional filters for profile_name and domain. + + Args: + domain: Filter by domain + + limit: Maximum number of results to return + + offset: Number of results to skip + + profile_name: Filter by profile name + + 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_api_list( + "/auth/connections", + page=SyncOffsetPagination[ManagedAuth], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "domain": domain, + "limit": limit, + "offset": offset, + "profile_name": profile_name, + }, + connection_list_params.ConnectionListParams, + ), + ), + model=ManagedAuth, + ) + + def delete( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """Deletes managed auth and terminates its workflow. + + This will: + + - Delete the managed auth record + - Terminate the Temporal workflow + - Cancel any in-progress login flows + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + f"/auth/connections/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + def follow( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Stream[ConnectionFollowResponse]: + """ + Establishes a Server-Sent Events (SSE) stream that delivers real-time login flow + state updates. The stream terminates automatically once the flow reaches a + terminal state (SUCCESS, FAILED, EXPIRED, CANCELED). + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} + return self._get( + f"/auth/connections/{id}/events", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=cast( + Any, ConnectionFollowResponse + ), # Union types cannot be passed in as arguments in the type system + stream=True, + stream_cls=Stream[ConnectionFollowResponse], + ) + + def login( + self, + id: str, + *, + save_credential_as: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> LoginResponse: + """Starts a login flow for the managed auth. + + Returns immediately with a hosted URL + for the user to complete authentication, or triggers automatic re-auth if + credentials are stored. + + Args: + save_credential_as: If provided, saves credentials under this name upon successful login + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/auth/connections/{id}/login", + body=maybe_transform( + {"save_credential_as": save_credential_as}, connection_login_params.ConnectionLoginParams + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=LoginResponse, + ) + + def submit( + self, + id: str, + *, + fields: Dict[str, str], + mfa_option_id: str | Omit = omit, + sso_button_selector: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SubmitFieldsResponse: + """Submits field values for the login form. + + Poll the managed auth to track progress + and get results. + + Args: + fields: Map of field name to value + + mfa_option_id: Optional MFA option ID if user selected an MFA method + + sso_button_selector: Optional XPath selector if user chose to click an SSO button instead + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/auth/connections/{id}/submit", + body=maybe_transform( + { + "fields": fields, + "mfa_option_id": mfa_option_id, + "sso_button_selector": sso_button_selector, + }, + connection_submit_params.ConnectionSubmitParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SubmitFieldsResponse, + ) + + +class AsyncConnectionsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncConnectionsResourceWithRawResponse: + """ + 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/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncConnectionsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncConnectionsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response + """ + return AsyncConnectionsResourceWithStreamingResponse(self) + + async def create( + self, + *, + domain: str, + profile_name: str, + allowed_domains: SequenceNotStr[str] | Omit = omit, + credential: connection_create_params.Credential | Omit = omit, + health_check_interval: int | Omit = omit, + login_url: str | Omit = omit, + proxy: connection_create_params.Proxy | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ManagedAuth: + """Creates managed authentication for a profile and domain combination. + + Returns 409 + Conflict if managed auth already exists for the given profile and domain. + + Args: + domain: Domain for authentication + + profile_name: Name of the profile to manage authentication for + + allowed_domains: Additional domains valid for this auth flow (besides the primary domain). Useful + when login pages redirect to different domains. + + The following SSO/OAuth provider domains are automatically allowed by default + and do not need to be specified: + + - Google: accounts.google.com + - Microsoft/Azure AD: login.microsoftonline.com, login.live.com + - Okta: _.okta.com, _.oktapreview.com + - Auth0: _.auth0.com, _.us.auth0.com, _.eu.auth0.com, _.au.auth0.com + - Apple: appleid.apple.com + - GitHub: github.com + - Facebook/Meta: www.facebook.com + - LinkedIn: www.linkedin.com + - Amazon Cognito: \\**.amazoncognito.com + - OneLogin: \\**.onelogin.com + - Ping Identity: _.pingone.com, _.pingidentity.com + + credential: + Reference to credentials for managed auth. Use one of: + + - { name } for Kernel credentials + - { provider, path } for external provider item + - { provider, auto: true } for external provider domain lookup + + health_check_interval: Interval in seconds between automatic health checks. When set, the system + periodically verifies the authentication status and triggers re-authentication + if needed. Must be between 300 (5 minutes) and 86400 (24 hours). Default is 3600 + (1 hour). + + login_url: Optional login page URL to skip discovery + + proxy: Optional proxy configuration + + 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( + "/auth/connections", + body=await async_maybe_transform( + { + "domain": domain, + "profile_name": profile_name, + "allowed_domains": allowed_domains, + "credential": credential, + "health_check_interval": health_check_interval, + "login_url": login_url, + "proxy": proxy, + }, + connection_create_params.ConnectionCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ManagedAuth, + ) + + async def retrieve( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ManagedAuth: + """Retrieve managed auth by its ID. + + Includes current flow state if a login is in + progress. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/auth/connections/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ManagedAuth, + ) + + def list( + self, + *, + domain: str | Omit = omit, + limit: int | Omit = omit, + offset: int | Omit = omit, + profile_name: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[ManagedAuth, AsyncOffsetPagination[ManagedAuth]]: + """ + List managed auths with optional filters for profile_name and domain. + + Args: + domain: Filter by domain + + limit: Maximum number of results to return + + offset: Number of results to skip + + profile_name: Filter by profile name + + 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_api_list( + "/auth/connections", + page=AsyncOffsetPagination[ManagedAuth], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "domain": domain, + "limit": limit, + "offset": offset, + "profile_name": profile_name, + }, + connection_list_params.ConnectionListParams, + ), + ), + model=ManagedAuth, + ) + + async def delete( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """Deletes managed auth and terminates its workflow. + + This will: + + - Delete the managed auth record + - Terminate the Temporal workflow + - Cancel any in-progress login flows + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + f"/auth/connections/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + async def follow( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncStream[ConnectionFollowResponse]: + """ + Establishes a Server-Sent Events (SSE) stream that delivers real-time login flow + state updates. The stream terminates automatically once the flow reaches a + terminal state (SUCCESS, FAILED, EXPIRED, CANCELED). + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} + return await self._get( + f"/auth/connections/{id}/events", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=cast( + Any, ConnectionFollowResponse + ), # Union types cannot be passed in as arguments in the type system + stream=True, + stream_cls=AsyncStream[ConnectionFollowResponse], + ) + + async def login( + self, + id: str, + *, + save_credential_as: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> LoginResponse: + """Starts a login flow for the managed auth. + + Returns immediately with a hosted URL + for the user to complete authentication, or triggers automatic re-auth if + credentials are stored. + + Args: + save_credential_as: If provided, saves credentials under this name upon successful login + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/auth/connections/{id}/login", + body=await async_maybe_transform( + {"save_credential_as": save_credential_as}, connection_login_params.ConnectionLoginParams + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=LoginResponse, + ) + + async def submit( + self, + id: str, + *, + fields: Dict[str, str], + mfa_option_id: str | Omit = omit, + sso_button_selector: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SubmitFieldsResponse: + """Submits field values for the login form. + + Poll the managed auth to track progress + and get results. + + Args: + fields: Map of field name to value + + mfa_option_id: Optional MFA option ID if user selected an MFA method + + sso_button_selector: Optional XPath selector if user chose to click an SSO button instead + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/auth/connections/{id}/submit", + body=await async_maybe_transform( + { + "fields": fields, + "mfa_option_id": mfa_option_id, + "sso_button_selector": sso_button_selector, + }, + connection_submit_params.ConnectionSubmitParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SubmitFieldsResponse, + ) + + +class ConnectionsResourceWithRawResponse: + def __init__(self, connections: ConnectionsResource) -> None: + self._connections = connections + + self.create = to_raw_response_wrapper( + connections.create, + ) + self.retrieve = to_raw_response_wrapper( + connections.retrieve, + ) + self.list = to_raw_response_wrapper( + connections.list, + ) + self.delete = to_raw_response_wrapper( + connections.delete, + ) + self.follow = to_raw_response_wrapper( + connections.follow, + ) + self.login = to_raw_response_wrapper( + connections.login, + ) + self.submit = to_raw_response_wrapper( + connections.submit, + ) + + +class AsyncConnectionsResourceWithRawResponse: + def __init__(self, connections: AsyncConnectionsResource) -> None: + self._connections = connections + + self.create = async_to_raw_response_wrapper( + connections.create, + ) + self.retrieve = async_to_raw_response_wrapper( + connections.retrieve, + ) + self.list = async_to_raw_response_wrapper( + connections.list, + ) + self.delete = async_to_raw_response_wrapper( + connections.delete, + ) + self.follow = async_to_raw_response_wrapper( + connections.follow, + ) + self.login = async_to_raw_response_wrapper( + connections.login, + ) + self.submit = async_to_raw_response_wrapper( + connections.submit, + ) + + +class ConnectionsResourceWithStreamingResponse: + def __init__(self, connections: ConnectionsResource) -> None: + self._connections = connections + + self.create = to_streamed_response_wrapper( + connections.create, + ) + self.retrieve = to_streamed_response_wrapper( + connections.retrieve, + ) + self.list = to_streamed_response_wrapper( + connections.list, + ) + self.delete = to_streamed_response_wrapper( + connections.delete, + ) + self.follow = to_streamed_response_wrapper( + connections.follow, + ) + self.login = to_streamed_response_wrapper( + connections.login, + ) + self.submit = to_streamed_response_wrapper( + connections.submit, + ) + + +class AsyncConnectionsResourceWithStreamingResponse: + def __init__(self, connections: AsyncConnectionsResource) -> None: + self._connections = connections + + self.create = async_to_streamed_response_wrapper( + connections.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + connections.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + connections.list, + ) + self.delete = async_to_streamed_response_wrapper( + connections.delete, + ) + self.follow = async_to_streamed_response_wrapper( + connections.follow, + ) + self.login = async_to_streamed_response_wrapper( + connections.login, + ) + self.submit = async_to_streamed_response_wrapper( + connections.submit, + ) diff --git a/src/kernel/resources/credential_providers.py b/src/kernel/resources/credential_providers.py index b1b1248c..8df7d55c 100644 --- a/src/kernel/resources/credential_providers.py +++ b/src/kernel/resources/credential_providers.py @@ -21,6 +21,7 @@ from ..types.credential_provider import CredentialProvider from ..types.credential_provider_test_result import CredentialProviderTestResult from ..types.credential_provider_list_response import CredentialProviderListResponse +from ..types.credential_provider_list_items_response import CredentialProviderListItemsResponse __all__ = ["CredentialProvidersResource", "AsyncCredentialProvidersResource"] @@ -49,6 +50,7 @@ def create( self, *, token: str, + name: str, provider_type: Literal["onepassword"], cache_ttl_seconds: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -65,6 +67,8 @@ def create( Args: token: Service account token for the provider (e.g., 1Password service account token) + name: Human-readable name for this provider instance (unique per org) + provider_type: Type of credential provider cache_ttl_seconds: How long to cache credential lists (default 300 seconds) @@ -78,10 +82,11 @@ def create( timeout: Override the client-level default timeout for this request, in seconds """ return self._post( - "/org/credential-providers", + "/org/credential_providers", body=maybe_transform( { "token": token, + "name": name, "provider_type": provider_type, "cache_ttl_seconds": cache_ttl_seconds, }, @@ -119,7 +124,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/org/credential-providers/{id}", + f"/org/credential_providers/{id}", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -133,6 +138,7 @@ def update( token: str | Omit = omit, cache_ttl_seconds: int | Omit = omit, enabled: bool | Omit = omit, + name: str | Omit = omit, priority: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -151,6 +157,8 @@ def update( enabled: Whether the provider is enabled for credential lookups + name: Human-readable name for this provider instance + priority: Priority order for credential lookups (lower numbers are checked first) extra_headers: Send extra headers @@ -164,12 +172,13 @@ def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._patch( - f"/org/credential-providers/{id}", + f"/org/credential_providers/{id}", body=maybe_transform( { "token": token, "cache_ttl_seconds": cache_ttl_seconds, "enabled": enabled, + "name": name, "priority": priority, }, credential_provider_update_params.CredentialProviderUpdateParams, @@ -192,7 +201,7 @@ def list( ) -> CredentialProviderListResponse: """List external credential providers configured for the organization.""" return self._get( - "/org/credential-providers", + "/org/credential_providers", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -226,13 +235,47 @@ def delete( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( - f"/org/credential-providers/{id}", + f"/org/credential_providers/{id}", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=NoneType, ) + def list_items( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> CredentialProviderListItemsResponse: + """ + Returns available credential items (e.g., 1Password login items) from the + provider. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/org/credential_providers/{id}/items", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CredentialProviderListItemsResponse, + ) + def test( self, id: str, @@ -259,7 +302,7 @@ def test( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/org/credential-providers/{id}/test", + f"/org/credential_providers/{id}/test", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -291,6 +334,7 @@ async def create( self, *, token: str, + name: str, provider_type: Literal["onepassword"], cache_ttl_seconds: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -307,6 +351,8 @@ async def create( Args: token: Service account token for the provider (e.g., 1Password service account token) + name: Human-readable name for this provider instance (unique per org) + provider_type: Type of credential provider cache_ttl_seconds: How long to cache credential lists (default 300 seconds) @@ -320,10 +366,11 @@ async def create( timeout: Override the client-level default timeout for this request, in seconds """ return await self._post( - "/org/credential-providers", + "/org/credential_providers", body=await async_maybe_transform( { "token": token, + "name": name, "provider_type": provider_type, "cache_ttl_seconds": cache_ttl_seconds, }, @@ -361,7 +408,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/org/credential-providers/{id}", + f"/org/credential_providers/{id}", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -375,6 +422,7 @@ async def update( token: str | Omit = omit, cache_ttl_seconds: int | Omit = omit, enabled: bool | Omit = omit, + name: str | Omit = omit, priority: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -393,6 +441,8 @@ async def update( enabled: Whether the provider is enabled for credential lookups + name: Human-readable name for this provider instance + priority: Priority order for credential lookups (lower numbers are checked first) extra_headers: Send extra headers @@ -406,12 +456,13 @@ async def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._patch( - f"/org/credential-providers/{id}", + f"/org/credential_providers/{id}", body=await async_maybe_transform( { "token": token, "cache_ttl_seconds": cache_ttl_seconds, "enabled": enabled, + "name": name, "priority": priority, }, credential_provider_update_params.CredentialProviderUpdateParams, @@ -434,7 +485,7 @@ async def list( ) -> CredentialProviderListResponse: """List external credential providers configured for the organization.""" return await self._get( - "/org/credential-providers", + "/org/credential_providers", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -468,13 +519,47 @@ async def delete( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( - f"/org/credential-providers/{id}", + f"/org/credential_providers/{id}", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=NoneType, ) + async def list_items( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> CredentialProviderListItemsResponse: + """ + Returns available credential items (e.g., 1Password login items) from the + provider. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/org/credential_providers/{id}/items", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CredentialProviderListItemsResponse, + ) + async def test( self, id: str, @@ -501,7 +586,7 @@ async def test( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/org/credential-providers/{id}/test", + f"/org/credential_providers/{id}/test", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -528,6 +613,9 @@ def __init__(self, credential_providers: CredentialProvidersResource) -> None: self.delete = to_raw_response_wrapper( credential_providers.delete, ) + self.list_items = to_raw_response_wrapper( + credential_providers.list_items, + ) self.test = to_raw_response_wrapper( credential_providers.test, ) @@ -552,6 +640,9 @@ def __init__(self, credential_providers: AsyncCredentialProvidersResource) -> No self.delete = async_to_raw_response_wrapper( credential_providers.delete, ) + self.list_items = async_to_raw_response_wrapper( + credential_providers.list_items, + ) self.test = async_to_raw_response_wrapper( credential_providers.test, ) @@ -576,6 +667,9 @@ def __init__(self, credential_providers: CredentialProvidersResource) -> None: self.delete = to_streamed_response_wrapper( credential_providers.delete, ) + self.list_items = to_streamed_response_wrapper( + credential_providers.list_items, + ) self.test = to_streamed_response_wrapper( credential_providers.test, ) @@ -600,6 +694,9 @@ def __init__(self, credential_providers: AsyncCredentialProvidersResource) -> No self.delete = async_to_streamed_response_wrapper( credential_providers.delete, ) + self.list_items = async_to_streamed_response_wrapper( + credential_providers.list_items, + ) self.test = async_to_streamed_response_wrapper( credential_providers.test, ) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 7e3ea2d6..6340848f 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -43,6 +43,7 @@ from .extension_upload_params import ExtensionUploadParams as ExtensionUploadParams from .proxy_retrieve_response import ProxyRetrieveResponse as ProxyRetrieveResponse from .credential_create_params import CredentialCreateParams as CredentialCreateParams +from .credential_provider_item import CredentialProviderItem as CredentialProviderItem from .credential_update_params import CredentialUpdateParams as CredentialUpdateParams from .deployment_create_params import DeploymentCreateParams as DeploymentCreateParams from .deployment_follow_params import DeploymentFollowParams as DeploymentFollowParams @@ -75,6 +76,9 @@ from .credential_provider_list_response import CredentialProviderListResponse as CredentialProviderListResponse from .credential_provider_update_params import CredentialProviderUpdateParams as CredentialProviderUpdateParams from .invocation_list_browsers_response import InvocationListBrowsersResponse as InvocationListBrowsersResponse +from .credential_provider_list_items_response import ( + CredentialProviderListItemsResponse as CredentialProviderListItemsResponse, +) from .extension_download_from_chrome_store_params import ( ExtensionDownloadFromChromeStoreParams as ExtensionDownloadFromChromeStoreParams, ) diff --git a/src/kernel/types/agents/agent_auth_invocation_response.py b/src/kernel/types/agents/agent_auth_invocation_response.py index 98235826..fa755492 100644 --- a/src/kernel/types/agents/agent_auth_invocation_response.py +++ b/src/kernel/types/agents/agent_auth_invocation_response.py @@ -16,8 +16,10 @@ class MfaOption(BaseModel): label: str """The visible option text""" - type: Literal["sms", "call", "email", "totp", "push", "security_key"] - """The MFA delivery method type""" + type: Literal["sms", "call", "email", "totp", "push", "password"] + """ + The MFA delivery method type (includes password for auth method selection pages) + """ description: Optional[str] = None """Additional instructions from the site""" @@ -59,12 +61,11 @@ class AgentAuthInvocationResponse(BaseModel): ] """Current step in the invocation workflow""" - type: Literal["login", "auto_login", "reauth"] - """The invocation type: + type: Literal["login", "reauth"] + """The session type: - - login: First-time authentication - - reauth: Re-authentication for previously authenticated agents - - auto_login: Legacy type (no longer created, kept for backward compatibility) + - login: User-initiated authentication + - reauth: System-triggered re-authentication (via health check) """ error_message: Optional[str] = None diff --git a/src/kernel/types/agents/auth/invocation_submit_params.py b/src/kernel/types/agents/auth/invocation_submit_params.py index 7a9c5aca..dc5ee5f6 100644 --- a/src/kernel/types/agents/auth/invocation_submit_params.py +++ b/src/kernel/types/agents/auth/invocation_submit_params.py @@ -19,8 +19,10 @@ class Variant1(TypedDict, total=False): class Variant2(TypedDict, total=False): - selected_mfa_type: Required[Literal["sms", "call", "email", "totp", "push", "security_key"]] - """The MFA delivery method type""" + selected_mfa_type: Required[Literal["sms", "call", "email", "totp", "push", "password"]] + """ + The MFA delivery method type (includes password for auth method selection pages) + """ InvocationSubmitParams: TypeAlias = Union[Variant0, Variant1, Variant2] diff --git a/src/kernel/types/agents/auth_agent.py b/src/kernel/types/agents/auth_agent.py index 7b23b689..851b55ce 100644 --- a/src/kernel/types/agents/auth_agent.py +++ b/src/kernel/types/agents/auth_agent.py @@ -6,7 +6,29 @@ from ..._models import BaseModel -__all__ = ["AuthAgent"] +__all__ = ["AuthAgent", "Credential"] + + +class Credential(BaseModel): + """Reference to credentials for managed auth. + + Use one of: + - { name } for Kernel credentials + - { provider, path } for external provider item + - { provider, auto: true } for external provider domain lookup + """ + + auto: Optional[bool] = None + """If true, lookup by domain from the specified provider""" + + name: Optional[str] = None + """Kernel credential name""" + + path: Optional[str] = None + """Provider-specific path (e.g., "VaultName/ItemName" for 1Password)""" + + provider: Optional[str] = None + """External provider name (e.g., "my-1p")""" class AuthAgent(BaseModel): @@ -31,6 +53,21 @@ class AuthAgent(BaseModel): Additional domains that are valid for this auth agent's authentication flow (besides the primary domain). Useful when login pages redirect to different domains. + + The following SSO/OAuth provider domains are automatically allowed by default + and do not need to be specified: + + - Google: accounts.google.com + - Microsoft/Azure AD: login.microsoftonline.com, login.live.com + - Okta: _.okta.com, _.oktapreview.com + - Auth0: _.auth0.com, _.us.auth0.com, _.eu.auth0.com, _.au.auth0.com + - Apple: appleid.apple.com + - GitHub: github.com + - Facebook/Meta: www.facebook.com + - LinkedIn: www.linkedin.com + - Amazon Cognito: \\**.amazoncognito.com + - OneLogin: \\**.onelogin.com + - Ping Identity: _.pingone.com, _.pingidentity.com """ can_reauth: Optional[bool] = None @@ -39,11 +76,19 @@ class AuthAgent(BaseModel): and login_url) """ - credential_id: Optional[str] = None - """ID of the linked credential for automatic re-authentication""" + credential: Optional[Credential] = None + """Reference to credentials for managed auth. Use one of: + + - { name } for Kernel credentials + - { provider, path } for external provider item + - { provider, auto: true } for external provider domain lookup + """ - credential_name: Optional[str] = None - """Name of the linked credential for automatic re-authentication""" + credential_id: Optional[str] = None + """ + ID of the linked Kernel credential for automatic re-authentication (deprecated, + use credential) + """ has_selectors: Optional[bool] = None """ diff --git a/src/kernel/types/agents/auth_agent_invocation_create_response.py b/src/kernel/types/agents/auth_agent_invocation_create_response.py index 6027f4d8..fc5d8dba 100644 --- a/src/kernel/types/agents/auth_agent_invocation_create_response.py +++ b/src/kernel/types/agents/auth_agent_invocation_create_response.py @@ -23,10 +23,9 @@ class AuthAgentInvocationCreateResponse(BaseModel): invocation_id: str """Unique identifier for the invocation.""" - type: Literal["login", "auto_login", "reauth"] - """The invocation type: + type: Literal["login", "reauth"] + """The session type: - - login: First-time authentication - - reauth: Re-authentication for previously authenticated agents - - auto_login: Legacy type (no longer created, kept for backward compatibility) + - login: User-initiated authentication + - reauth: System-triggered re-authentication (via health check) """ diff --git a/src/kernel/types/agents/auth_create_params.py b/src/kernel/types/agents/auth_create_params.py index b792d566..613f0034 100644 --- a/src/kernel/types/agents/auth_create_params.py +++ b/src/kernel/types/agents/auth_create_params.py @@ -21,6 +21,21 @@ class AuthCreateParams(TypedDict, total=False): Additional domains that are valid for this auth agent's authentication flow (besides the primary domain). Useful when login pages redirect to different domains. + + The following SSO/OAuth provider domains are automatically allowed by default + and do not need to be specified: + + - Google: accounts.google.com + - Microsoft/Azure AD: login.microsoftonline.com, login.live.com + - Okta: _.okta.com, _.oktapreview.com + - Auth0: _.auth0.com, _.us.auth0.com, _.eu.auth0.com, _.au.auth0.com + - Apple: appleid.apple.com + - GitHub: github.com + - Facebook/Meta: www.facebook.com + - LinkedIn: www.linkedin.com + - Amazon Cognito: \\**.amazoncognito.com + - OneLogin: \\**.onelogin.com + - Ping Identity: _.pingone.com, _.pingidentity.com """ credential_name: str diff --git a/src/kernel/types/agents/discovered_field.py b/src/kernel/types/agents/discovered_field.py index 72ac2949..4dbf53a8 100644 --- a/src/kernel/types/agents/discovered_field.py +++ b/src/kernel/types/agents/discovered_field.py @@ -23,6 +23,12 @@ class DiscoveredField(BaseModel): type: Literal["text", "email", "password", "tel", "number", "url", "code", "totp"] """Field type""" + linked_mfa_type: Optional[Literal["sms", "call", "email", "totp", "push", "password"]] = None + """ + If this field is associated with an MFA option, the type of that option (e.g., + password field linked to "Enter password" option) + """ + placeholder: Optional[str] = None """Field placeholder""" diff --git a/src/kernel/types/auth/__init__.py b/src/kernel/types/auth/__init__.py new file mode 100644 index 00000000..51e505bf --- /dev/null +++ b/src/kernel/types/auth/__init__.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .managed_auth import ManagedAuth as ManagedAuth +from .login_response import LoginResponse as LoginResponse +from .connection_list_params import ConnectionListParams as ConnectionListParams +from .submit_fields_response import SubmitFieldsResponse as SubmitFieldsResponse +from .connection_login_params import ConnectionLoginParams as ConnectionLoginParams +from .connection_create_params import ConnectionCreateParams as ConnectionCreateParams +from .connection_submit_params import ConnectionSubmitParams as ConnectionSubmitParams +from .connection_follow_response import ConnectionFollowResponse as ConnectionFollowResponse diff --git a/src/kernel/types/auth/connection_create_params.py b/src/kernel/types/auth/connection_create_params.py new file mode 100644 index 00000000..9282f48d --- /dev/null +++ b/src/kernel/types/auth/connection_create_params.py @@ -0,0 +1,89 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +from ..._types import SequenceNotStr + +__all__ = ["ConnectionCreateParams", "Credential", "Proxy"] + + +class ConnectionCreateParams(TypedDict, total=False): + domain: Required[str] + """Domain for authentication""" + + profile_name: Required[str] + """Name of the profile to manage authentication for""" + + allowed_domains: SequenceNotStr[str] + """Additional domains valid for this auth flow (besides the primary domain). + + Useful when login pages redirect to different domains. + + The following SSO/OAuth provider domains are automatically allowed by default + and do not need to be specified: + + - Google: accounts.google.com + - Microsoft/Azure AD: login.microsoftonline.com, login.live.com + - Okta: _.okta.com, _.oktapreview.com + - Auth0: _.auth0.com, _.us.auth0.com, _.eu.auth0.com, _.au.auth0.com + - Apple: appleid.apple.com + - GitHub: github.com + - Facebook/Meta: www.facebook.com + - LinkedIn: www.linkedin.com + - Amazon Cognito: \\**.amazoncognito.com + - OneLogin: \\**.onelogin.com + - Ping Identity: _.pingone.com, _.pingidentity.com + """ + + credential: Credential + """Reference to credentials for managed auth. Use one of: + + - { name } for Kernel credentials + - { provider, path } for external provider item + - { provider, auto: true } for external provider domain lookup + """ + + health_check_interval: int + """Interval in seconds between automatic health checks. + + When set, the system periodically verifies the authentication status and + triggers re-authentication if needed. Must be between 300 (5 minutes) and 86400 + (24 hours). Default is 3600 (1 hour). + """ + + login_url: str + """Optional login page URL to skip discovery""" + + proxy: Proxy + """Optional proxy configuration""" + + +class Credential(TypedDict, total=False): + """Reference to credentials for managed auth. + + Use one of: + - { name } for Kernel credentials + - { provider, path } for external provider item + - { provider, auto: true } for external provider domain lookup + """ + + auto: bool + """If true, lookup by domain from the specified provider""" + + name: str + """Kernel credential name""" + + path: str + """Provider-specific path (e.g., "VaultName/ItemName" for 1Password)""" + + provider: str + """External provider name (e.g., "my-1p")""" + + +class Proxy(TypedDict, total=False): + """Optional proxy configuration""" + + proxy_id: str + """ID of the proxy to use""" diff --git a/src/kernel/types/auth/connection_follow_response.py b/src/kernel/types/auth/connection_follow_response.py new file mode 100644 index 00000000..488ec81a --- /dev/null +++ b/src/kernel/types/auth/connection_follow_response.py @@ -0,0 +1,109 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Union, Optional +from datetime import datetime +from typing_extensions import Literal, Annotated, TypeAlias + +from ..._utils import PropertyInfo +from ..._models import BaseModel +from ..shared.error_event import ErrorEvent +from ..shared.heartbeat_event import HeartbeatEvent +from ..agents.discovered_field import DiscoveredField + +__all__ = [ + "ConnectionFollowResponse", + "ManagedAuthStateEvent", + "ManagedAuthStateEventMfaOption", + "ManagedAuthStateEventPendingSSOButton", +] + + +class ManagedAuthStateEventMfaOption(BaseModel): + """An MFA method option for verification""" + + label: str + """The visible option text""" + + type: Literal["sms", "call", "email", "totp", "push", "password"] + """ + The MFA delivery method type (includes password for auth method selection pages) + """ + + description: Optional[str] = None + """Additional instructions from the site""" + + target: Optional[str] = None + """The masked destination (phone/email) if shown""" + + +class ManagedAuthStateEventPendingSSOButton(BaseModel): + """An SSO button for signing in with an external identity provider""" + + label: str + """Visible button text""" + + provider: str + """Identity provider name""" + + selector: str + """XPath selector for the button""" + + +class ManagedAuthStateEvent(BaseModel): + """An event representing the current state of a managed auth flow.""" + + event: Literal["managed_auth_state"] + """Event type identifier (always "managed_auth_state").""" + + flow_status: Literal["IN_PROGRESS", "SUCCESS", "FAILED", "EXPIRED", "CANCELED"] + """Current flow status.""" + + flow_step: Literal["DISCOVERING", "AWAITING_INPUT", "AWAITING_EXTERNAL_ACTION", "SUBMITTING", "COMPLETED"] + """Current step in the flow.""" + + timestamp: datetime + """Time the state was reported.""" + + discovered_fields: Optional[List[DiscoveredField]] = None + """Fields awaiting input (present when flow_step=AWAITING_INPUT).""" + + error_message: Optional[str] = None + """Error message (present when flow_status=FAILED).""" + + external_action_message: Optional[str] = None + """ + Instructions for external action (present when + flow_step=AWAITING_EXTERNAL_ACTION). + """ + + flow_type: Optional[Literal["LOGIN", "REAUTH"]] = None + """Type of the current flow.""" + + hosted_url: Optional[str] = None + """URL to redirect user to for hosted login.""" + + live_view_url: Optional[str] = None + """Browser live view URL for debugging.""" + + mfa_options: Optional[List[ManagedAuthStateEventMfaOption]] = None + """ + MFA method options (present when flow_step=AWAITING_INPUT and MFA selection + required). + """ + + pending_sso_buttons: Optional[List[ManagedAuthStateEventPendingSSOButton]] = None + """SSO buttons available (present when flow_step=AWAITING_INPUT).""" + + post_login_url: Optional[str] = None + """URL where the browser landed after successful login.""" + + website_error: Optional[str] = None + """Visible error message from the website (e.g., 'Incorrect password'). + + Present when the website displays an error during login. + """ + + +ConnectionFollowResponse: TypeAlias = Annotated[ + Union[ManagedAuthStateEvent, ErrorEvent, HeartbeatEvent], PropertyInfo(discriminator="event") +] diff --git a/src/kernel/types/auth/connection_list_params.py b/src/kernel/types/auth/connection_list_params.py new file mode 100644 index 00000000..dd8d0de2 --- /dev/null +++ b/src/kernel/types/auth/connection_list_params.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["ConnectionListParams"] + + +class ConnectionListParams(TypedDict, total=False): + domain: str + """Filter by domain""" + + limit: int + """Maximum number of results to return""" + + offset: int + """Number of results to skip""" + + profile_name: str + """Filter by profile name""" diff --git a/src/kernel/types/auth/connection_login_params.py b/src/kernel/types/auth/connection_login_params.py new file mode 100644 index 00000000..a30d9ff4 --- /dev/null +++ b/src/kernel/types/auth/connection_login_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["ConnectionLoginParams"] + + +class ConnectionLoginParams(TypedDict, total=False): + save_credential_as: str + """If provided, saves credentials under this name upon successful login""" diff --git a/src/kernel/types/auth/connection_submit_params.py b/src/kernel/types/auth/connection_submit_params.py new file mode 100644 index 00000000..b299e0a5 --- /dev/null +++ b/src/kernel/types/auth/connection_submit_params.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict +from typing_extensions import Required, TypedDict + +__all__ = ["ConnectionSubmitParams"] + + +class ConnectionSubmitParams(TypedDict, total=False): + fields: Required[Dict[str, str]] + """Map of field name to value""" + + mfa_option_id: str + """Optional MFA option ID if user selected an MFA method""" + + sso_button_selector: str + """Optional XPath selector if user chose to click an SSO button instead""" diff --git a/src/kernel/types/auth/login_response.py b/src/kernel/types/auth/login_response.py new file mode 100644 index 00000000..178c3005 --- /dev/null +++ b/src/kernel/types/auth/login_response.py @@ -0,0 +1,31 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["LoginResponse"] + + +class LoginResponse(BaseModel): + """Response from starting a login flow""" + + id: str + """Managed auth ID""" + + flow_expires_at: datetime + """When the login flow expires""" + + flow_type: Literal["LOGIN", "REAUTH"] + """Type of login flow started""" + + hosted_url: str + """URL to redirect user to for login""" + + handoff_code: Optional[str] = None + """One-time code for handoff (internal use)""" + + live_view_url: Optional[str] = None + """Browser live view URL for watching the login flow""" diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py new file mode 100644 index 00000000..849616fb --- /dev/null +++ b/src/kernel/types/auth/managed_auth.py @@ -0,0 +1,181 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from datetime import datetime +from typing_extensions import Literal + +from ..._models import BaseModel +from ..agents.discovered_field import DiscoveredField + +__all__ = ["ManagedAuth", "Credential", "MfaOption", "PendingSSOButton"] + + +class Credential(BaseModel): + """Reference to credentials for managed auth. + + Use one of: + - { name } for Kernel credentials + - { provider, path } for external provider item + - { provider, auto: true } for external provider domain lookup + """ + + auto: Optional[bool] = None + """If true, lookup by domain from the specified provider""" + + name: Optional[str] = None + """Kernel credential name""" + + path: Optional[str] = None + """Provider-specific path (e.g., "VaultName/ItemName" for 1Password)""" + + provider: Optional[str] = None + """External provider name (e.g., "my-1p")""" + + +class MfaOption(BaseModel): + """An MFA method option for verification""" + + label: str + """The visible option text""" + + type: Literal["sms", "call", "email", "totp", "push", "password"] + """ + The MFA delivery method type (includes password for auth method selection pages) + """ + + description: Optional[str] = None + """Additional instructions from the site""" + + target: Optional[str] = None + """The masked destination (phone/email) if shown""" + + +class PendingSSOButton(BaseModel): + """An SSO button for signing in with an external identity provider""" + + label: str + """Visible button text""" + + provider: str + """Identity provider name""" + + selector: str + """XPath selector for the button""" + + +class ManagedAuth(BaseModel): + """Managed authentication that keeps a profile logged into a specific domain. + + Flow fields (flow_status, flow_step, discovered_fields, mfa_options) reflect the most recent login flow and are null when no flow has been initiated. + """ + + id: str + """Unique identifier for the managed auth""" + + domain: str + """Target domain for authentication""" + + profile_name: str + """Name of the profile associated with this managed auth""" + + status: Literal["AUTHENTICATED", "NEEDS_AUTH"] + """Current authentication status of the managed profile""" + + allowed_domains: Optional[List[str]] = None + """ + Additional domains that are valid for this auth flow (besides the primary + domain). Useful when login pages redirect to different domains. + + The following SSO/OAuth provider domains are automatically allowed by default + and do not need to be specified: + + - Google: accounts.google.com + - Microsoft/Azure AD: login.microsoftonline.com, login.live.com + - Okta: _.okta.com, _.oktapreview.com + - Auth0: _.auth0.com, _.us.auth0.com, _.eu.auth0.com, _.au.auth0.com + - Apple: appleid.apple.com + - GitHub: github.com + - Facebook/Meta: www.facebook.com + - LinkedIn: www.linkedin.com + - Amazon Cognito: \\**.amazoncognito.com + - OneLogin: \\**.onelogin.com + - Ping Identity: _.pingone.com, _.pingidentity.com + """ + + can_reauth: Optional[bool] = None + """ + Whether automatic re-authentication is possible (has credential, selectors, and + login_url) + """ + + credential: Optional[Credential] = None + """Reference to credentials for managed auth. Use one of: + + - { name } for Kernel credentials + - { provider, path } for external provider item + - { provider, auto: true } for external provider domain lookup + """ + + discovered_fields: Optional[List[DiscoveredField]] = None + """Fields awaiting input (present when flow_step=awaiting_input)""" + + error_message: Optional[str] = None + """Error message (present when flow_status=failed)""" + + external_action_message: Optional[str] = None + """ + Instructions for external action (present when + flow_step=awaiting_external_action) + """ + + flow_expires_at: Optional[datetime] = None + """When the current flow expires (null when no flow in progress)""" + + flow_status: Optional[Literal["IN_PROGRESS", "SUCCESS", "FAILED", "EXPIRED", "CANCELED"]] = None + """Current flow status (null when no flow in progress)""" + + flow_step: Optional[ + Literal["DISCOVERING", "AWAITING_INPUT", "AWAITING_EXTERNAL_ACTION", "SUBMITTING", "COMPLETED"] + ] = None + """Current step in the flow (null when no flow in progress)""" + + flow_type: Optional[Literal["LOGIN", "REAUTH"]] = None + """Type of the current flow (null when no flow in progress)""" + + health_check_interval: Optional[int] = None + """Interval in seconds between automatic health checks. + + When set, the system periodically verifies the authentication status and + triggers re-authentication if needed. Must be between 300 (5 minutes) and 86400 + (24 hours). Default is 3600 (1 hour). + """ + + hosted_url: Optional[str] = None + """URL to redirect user to for hosted login (present when flow in progress)""" + + last_auth_at: Optional[datetime] = None + """When the profile was last successfully authenticated""" + + live_view_url: Optional[str] = None + """Browser live view URL for debugging (present when flow in progress)""" + + mfa_options: Optional[List[MfaOption]] = None + """ + MFA method options (present when flow_step=awaiting_input and MFA selection + required) + """ + + pending_sso_buttons: Optional[List[PendingSSOButton]] = None + """SSO buttons available (present when flow_step=awaiting_input)""" + + post_login_url: Optional[str] = None + """URL where the browser landed after successful login""" + + sso_provider: Optional[str] = None + """SSO provider being used (e.g., google, github, microsoft)""" + + website_error: Optional[str] = None + """Visible error message from the website (e.g., 'Incorrect password'). + + Present when the website displays an error during login. + """ diff --git a/src/kernel/types/auth/submit_fields_response.py b/src/kernel/types/auth/submit_fields_response.py new file mode 100644 index 00000000..1133c1b4 --- /dev/null +++ b/src/kernel/types/auth/submit_fields_response.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from ..._models import BaseModel + +__all__ = ["SubmitFieldsResponse"] + + +class SubmitFieldsResponse(BaseModel): + """Response from submitting field values""" + + accepted: bool + """Whether the submission was accepted for processing""" diff --git a/src/kernel/types/credential_provider.py b/src/kernel/types/credential_provider.py index 83a205ad..866f0631 100644 --- a/src/kernel/types/credential_provider.py +++ b/src/kernel/types/credential_provider.py @@ -22,6 +22,9 @@ class CredentialProvider(BaseModel): enabled: bool """Whether the provider is enabled for credential lookups""" + name: str + """Human-readable name for this provider instance""" + priority: int """Priority order for credential lookups (lower numbers are checked first)""" diff --git a/src/kernel/types/credential_provider_create_params.py b/src/kernel/types/credential_provider_create_params.py index ed631b39..ea531c7d 100644 --- a/src/kernel/types/credential_provider_create_params.py +++ b/src/kernel/types/credential_provider_create_params.py @@ -11,6 +11,9 @@ class CredentialProviderCreateParams(TypedDict, total=False): token: Required[str] """Service account token for the provider (e.g., 1Password service account token)""" + name: Required[str] + """Human-readable name for this provider instance (unique per org)""" + provider_type: Required[Literal["onepassword"]] """Type of credential provider""" diff --git a/src/kernel/types/credential_provider_item.py b/src/kernel/types/credential_provider_item.py new file mode 100644 index 00000000..8bc618b5 --- /dev/null +++ b/src/kernel/types/credential_provider_item.py @@ -0,0 +1,29 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel + +__all__ = ["CredentialProviderItem"] + + +class CredentialProviderItem(BaseModel): + """A credential item from an external provider (e.g., a 1Password login item)""" + + id: str + """Unique identifier for the item within the provider""" + + path: str + """Path to reference this item (VaultName/ItemTitle format)""" + + title: str + """Display name of the credential item""" + + vault_id: str + """ID of the vault containing this item""" + + vault_name: str + """Name of the vault containing this item""" + + urls: Optional[List[str]] = None + """URLs associated with this credential""" diff --git a/src/kernel/types/credential_provider_list_items_response.py b/src/kernel/types/credential_provider_list_items_response.py new file mode 100644 index 00000000..265f2ef2 --- /dev/null +++ b/src/kernel/types/credential_provider_list_items_response.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel +from .credential_provider_item import CredentialProviderItem + +__all__ = ["CredentialProviderListItemsResponse"] + + +class CredentialProviderListItemsResponse(BaseModel): + items: Optional[List[CredentialProviderItem]] = None diff --git a/src/kernel/types/credential_provider_update_params.py b/src/kernel/types/credential_provider_update_params.py index ecebeab7..b2dda02b 100644 --- a/src/kernel/types/credential_provider_update_params.py +++ b/src/kernel/types/credential_provider_update_params.py @@ -17,5 +17,8 @@ class CredentialProviderUpdateParams(TypedDict, total=False): enabled: bool """Whether the provider is enabled for credential lookups""" + name: str + """Human-readable name for this provider instance""" + priority: int """Priority order for credential lookups (lower numbers are checked first)""" diff --git a/tests/api_resources/agents/auth/test_invocations.py b/tests/api_resources/agents/auth/test_invocations.py index 6d70dfac..829a6076 100644 --- a/tests/api_resources/agents/auth/test_invocations.py +++ b/tests/api_resources/agents/auth/test_invocations.py @@ -14,6 +14,8 @@ InvocationExchangeResponse, ) +# pyright: reportDeprecated=false + base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -23,26 +25,31 @@ class TestInvocations: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_create(self, client: Kernel) -> None: - invocation = client.agents.auth.invocations.create( - auth_agent_id="abc123xyz", - ) + with pytest.warns(DeprecationWarning): + invocation = client.agents.auth.invocations.create( + auth_agent_id="abc123xyz", + ) + assert_matches_type(AuthAgentInvocationCreateResponse, invocation, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_create_with_all_params(self, client: Kernel) -> None: - invocation = client.agents.auth.invocations.create( - auth_agent_id="abc123xyz", - save_credential_as="my-netflix-login", - ) + with pytest.warns(DeprecationWarning): + invocation = client.agents.auth.invocations.create( + auth_agent_id="abc123xyz", + save_credential_as="my-netflix-login", + ) + assert_matches_type(AuthAgentInvocationCreateResponse, invocation, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_create(self, client: Kernel) -> None: - response = client.agents.auth.invocations.with_raw_response.create( - auth_agent_id="abc123xyz", - ) + with pytest.warns(DeprecationWarning): + response = client.agents.auth.invocations.with_raw_response.create( + auth_agent_id="abc123xyz", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -52,31 +59,35 @@ def test_raw_response_create(self, client: Kernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_create(self, client: Kernel) -> None: - with client.agents.auth.invocations.with_streaming_response.create( - auth_agent_id="abc123xyz", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + with client.agents.auth.invocations.with_streaming_response.create( + auth_agent_id="abc123xyz", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - invocation = response.parse() - assert_matches_type(AuthAgentInvocationCreateResponse, invocation, path=["response"]) + invocation = response.parse() + assert_matches_type(AuthAgentInvocationCreateResponse, invocation, 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: Kernel) -> None: - invocation = client.agents.auth.invocations.retrieve( - "invocation_id", - ) + with pytest.warns(DeprecationWarning): + invocation = client.agents.auth.invocations.retrieve( + "invocation_id", + ) + assert_matches_type(AgentAuthInvocationResponse, invocation, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_retrieve(self, client: Kernel) -> None: - response = client.agents.auth.invocations.with_raw_response.retrieve( - "invocation_id", - ) + with pytest.warns(DeprecationWarning): + response = client.agents.auth.invocations.with_raw_response.retrieve( + "invocation_id", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -86,41 +97,46 @@ def test_raw_response_retrieve(self, client: Kernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_retrieve(self, client: Kernel) -> None: - with client.agents.auth.invocations.with_streaming_response.retrieve( - "invocation_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + with client.agents.auth.invocations.with_streaming_response.retrieve( + "invocation_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - invocation = response.parse() - assert_matches_type(AgentAuthInvocationResponse, invocation, path=["response"]) + invocation = response.parse() + assert_matches_type(AgentAuthInvocationResponse, invocation, 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: Kernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): - client.agents.auth.invocations.with_raw_response.retrieve( - "", - ) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): + client.agents.auth.invocations.with_raw_response.retrieve( + "", + ) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_exchange(self, client: Kernel) -> None: - invocation = client.agents.auth.invocations.exchange( - invocation_id="invocation_id", - code="abc123xyz", - ) + with pytest.warns(DeprecationWarning): + invocation = client.agents.auth.invocations.exchange( + invocation_id="invocation_id", + code="abc123xyz", + ) + assert_matches_type(InvocationExchangeResponse, invocation, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_exchange(self, client: Kernel) -> None: - response = client.agents.auth.invocations.with_raw_response.exchange( - invocation_id="invocation_id", - code="abc123xyz", - ) + with pytest.warns(DeprecationWarning): + response = client.agents.auth.invocations.with_raw_response.exchange( + invocation_id="invocation_id", + code="abc123xyz", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -130,49 +146,54 @@ def test_raw_response_exchange(self, client: Kernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_exchange(self, client: Kernel) -> None: - with client.agents.auth.invocations.with_streaming_response.exchange( - invocation_id="invocation_id", - code="abc123xyz", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + with client.agents.auth.invocations.with_streaming_response.exchange( + invocation_id="invocation_id", + code="abc123xyz", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - invocation = response.parse() - assert_matches_type(InvocationExchangeResponse, invocation, path=["response"]) + invocation = response.parse() + assert_matches_type(InvocationExchangeResponse, invocation, path=["response"]) assert cast(Any, response.is_closed) is True @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_path_params_exchange(self, client: Kernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): - client.agents.auth.invocations.with_raw_response.exchange( - invocation_id="", - code="abc123xyz", - ) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): + client.agents.auth.invocations.with_raw_response.exchange( + invocation_id="", + code="abc123xyz", + ) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_submit_overload_1(self, client: Kernel) -> None: - invocation = client.agents.auth.invocations.submit( - invocation_id="invocation_id", - field_values={ - "email": "user@example.com", - "password": "********", - }, - ) + with pytest.warns(DeprecationWarning): + invocation = client.agents.auth.invocations.submit( + invocation_id="invocation_id", + field_values={ + "email": "user@example.com", + "password": "********", + }, + ) + assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_submit_overload_1(self, client: Kernel) -> None: - response = client.agents.auth.invocations.with_raw_response.submit( - invocation_id="invocation_id", - field_values={ - "email": "user@example.com", - "password": "********", - }, - ) + with pytest.warns(DeprecationWarning): + response = client.agents.auth.invocations.with_raw_response.submit( + invocation_id="invocation_id", + field_values={ + "email": "user@example.com", + "password": "********", + }, + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -182,49 +203,54 @@ def test_raw_response_submit_overload_1(self, client: Kernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_submit_overload_1(self, client: Kernel) -> None: - with client.agents.auth.invocations.with_streaming_response.submit( - invocation_id="invocation_id", - field_values={ - "email": "user@example.com", - "password": "********", - }, - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - invocation = response.parse() - assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) + with pytest.warns(DeprecationWarning): + with client.agents.auth.invocations.with_streaming_response.submit( + invocation_id="invocation_id", + field_values={ + "email": "user@example.com", + "password": "********", + }, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + invocation = response.parse() + assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) assert cast(Any, response.is_closed) is True @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_path_params_submit_overload_1(self, client: Kernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): - client.agents.auth.invocations.with_raw_response.submit( - invocation_id="", - field_values={ - "email": "user@example.com", - "password": "********", - }, - ) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): + client.agents.auth.invocations.with_raw_response.submit( + invocation_id="", + field_values={ + "email": "user@example.com", + "password": "********", + }, + ) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_submit_overload_2(self, client: Kernel) -> None: - invocation = client.agents.auth.invocations.submit( - invocation_id="invocation_id", - sso_button="xpath=//button[contains(text(), 'Continue with Google')]", - ) + with pytest.warns(DeprecationWarning): + invocation = client.agents.auth.invocations.submit( + invocation_id="invocation_id", + sso_button="xpath=//button[contains(text(), 'Continue with Google')]", + ) + assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_submit_overload_2(self, client: Kernel) -> None: - response = client.agents.auth.invocations.with_raw_response.submit( - invocation_id="invocation_id", - sso_button="xpath=//button[contains(text(), 'Continue with Google')]", - ) + with pytest.warns(DeprecationWarning): + response = client.agents.auth.invocations.with_raw_response.submit( + invocation_id="invocation_id", + sso_button="xpath=//button[contains(text(), 'Continue with Google')]", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -234,43 +260,48 @@ def test_raw_response_submit_overload_2(self, client: Kernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_submit_overload_2(self, client: Kernel) -> None: - with client.agents.auth.invocations.with_streaming_response.submit( - invocation_id="invocation_id", - sso_button="xpath=//button[contains(text(), 'Continue with Google')]", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + with client.agents.auth.invocations.with_streaming_response.submit( + invocation_id="invocation_id", + sso_button="xpath=//button[contains(text(), 'Continue with Google')]", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - invocation = response.parse() - assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) + invocation = response.parse() + assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) assert cast(Any, response.is_closed) is True @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_path_params_submit_overload_2(self, client: Kernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): - client.agents.auth.invocations.with_raw_response.submit( - invocation_id="", - sso_button="xpath=//button[contains(text(), 'Continue with Google')]", - ) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): + client.agents.auth.invocations.with_raw_response.submit( + invocation_id="", + sso_button="xpath=//button[contains(text(), 'Continue with Google')]", + ) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_submit_overload_3(self, client: Kernel) -> None: - invocation = client.agents.auth.invocations.submit( - invocation_id="invocation_id", - selected_mfa_type="sms", - ) + with pytest.warns(DeprecationWarning): + invocation = client.agents.auth.invocations.submit( + invocation_id="invocation_id", + selected_mfa_type="sms", + ) + assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_submit_overload_3(self, client: Kernel) -> None: - response = client.agents.auth.invocations.with_raw_response.submit( - invocation_id="invocation_id", - selected_mfa_type="sms", - ) + with pytest.warns(DeprecationWarning): + response = client.agents.auth.invocations.with_raw_response.submit( + invocation_id="invocation_id", + selected_mfa_type="sms", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -280,26 +311,28 @@ def test_raw_response_submit_overload_3(self, client: Kernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_submit_overload_3(self, client: Kernel) -> None: - with client.agents.auth.invocations.with_streaming_response.submit( - invocation_id="invocation_id", - selected_mfa_type="sms", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + with client.agents.auth.invocations.with_streaming_response.submit( + invocation_id="invocation_id", + selected_mfa_type="sms", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - invocation = response.parse() - assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) + invocation = response.parse() + assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) assert cast(Any, response.is_closed) is True @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_path_params_submit_overload_3(self, client: Kernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): - client.agents.auth.invocations.with_raw_response.submit( - invocation_id="", - selected_mfa_type="sms", - ) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): + client.agents.auth.invocations.with_raw_response.submit( + invocation_id="", + selected_mfa_type="sms", + ) class TestAsyncInvocations: @@ -310,26 +343,31 @@ class TestAsyncInvocations: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_create(self, async_client: AsyncKernel) -> None: - invocation = await async_client.agents.auth.invocations.create( - auth_agent_id="abc123xyz", - ) + with pytest.warns(DeprecationWarning): + invocation = await async_client.agents.auth.invocations.create( + auth_agent_id="abc123xyz", + ) + assert_matches_type(AuthAgentInvocationCreateResponse, invocation, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: - invocation = await async_client.agents.auth.invocations.create( - auth_agent_id="abc123xyz", - save_credential_as="my-netflix-login", - ) + with pytest.warns(DeprecationWarning): + invocation = await async_client.agents.auth.invocations.create( + auth_agent_id="abc123xyz", + save_credential_as="my-netflix-login", + ) + assert_matches_type(AuthAgentInvocationCreateResponse, invocation, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_create(self, async_client: AsyncKernel) -> None: - response = await async_client.agents.auth.invocations.with_raw_response.create( - auth_agent_id="abc123xyz", - ) + with pytest.warns(DeprecationWarning): + response = await async_client.agents.auth.invocations.with_raw_response.create( + auth_agent_id="abc123xyz", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -339,31 +377,35 @@ async def test_raw_response_create(self, async_client: AsyncKernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: - async with async_client.agents.auth.invocations.with_streaming_response.create( - auth_agent_id="abc123xyz", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + async with async_client.agents.auth.invocations.with_streaming_response.create( + auth_agent_id="abc123xyz", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - invocation = await response.parse() - assert_matches_type(AuthAgentInvocationCreateResponse, invocation, path=["response"]) + invocation = await response.parse() + assert_matches_type(AuthAgentInvocationCreateResponse, invocation, 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: AsyncKernel) -> None: - invocation = await async_client.agents.auth.invocations.retrieve( - "invocation_id", - ) + with pytest.warns(DeprecationWarning): + invocation = await async_client.agents.auth.invocations.retrieve( + "invocation_id", + ) + assert_matches_type(AgentAuthInvocationResponse, invocation, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: - response = await async_client.agents.auth.invocations.with_raw_response.retrieve( - "invocation_id", - ) + with pytest.warns(DeprecationWarning): + response = await async_client.agents.auth.invocations.with_raw_response.retrieve( + "invocation_id", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -373,41 +415,46 @@ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: - async with async_client.agents.auth.invocations.with_streaming_response.retrieve( - "invocation_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + async with async_client.agents.auth.invocations.with_streaming_response.retrieve( + "invocation_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - invocation = await response.parse() - assert_matches_type(AgentAuthInvocationResponse, invocation, path=["response"]) + invocation = await response.parse() + assert_matches_type(AgentAuthInvocationResponse, invocation, 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: AsyncKernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): - await async_client.agents.auth.invocations.with_raw_response.retrieve( - "", - ) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): + await async_client.agents.auth.invocations.with_raw_response.retrieve( + "", + ) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_exchange(self, async_client: AsyncKernel) -> None: - invocation = await async_client.agents.auth.invocations.exchange( - invocation_id="invocation_id", - code="abc123xyz", - ) + with pytest.warns(DeprecationWarning): + invocation = await async_client.agents.auth.invocations.exchange( + invocation_id="invocation_id", + code="abc123xyz", + ) + assert_matches_type(InvocationExchangeResponse, invocation, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_exchange(self, async_client: AsyncKernel) -> None: - response = await async_client.agents.auth.invocations.with_raw_response.exchange( - invocation_id="invocation_id", - code="abc123xyz", - ) + with pytest.warns(DeprecationWarning): + response = await async_client.agents.auth.invocations.with_raw_response.exchange( + invocation_id="invocation_id", + code="abc123xyz", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -417,49 +464,54 @@ async def test_raw_response_exchange(self, async_client: AsyncKernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_exchange(self, async_client: AsyncKernel) -> None: - async with async_client.agents.auth.invocations.with_streaming_response.exchange( - invocation_id="invocation_id", - code="abc123xyz", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + async with async_client.agents.auth.invocations.with_streaming_response.exchange( + invocation_id="invocation_id", + code="abc123xyz", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - invocation = await response.parse() - assert_matches_type(InvocationExchangeResponse, invocation, path=["response"]) + invocation = await response.parse() + assert_matches_type(InvocationExchangeResponse, invocation, path=["response"]) assert cast(Any, response.is_closed) is True @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_path_params_exchange(self, async_client: AsyncKernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): - await async_client.agents.auth.invocations.with_raw_response.exchange( - invocation_id="", - code="abc123xyz", - ) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): + await async_client.agents.auth.invocations.with_raw_response.exchange( + invocation_id="", + code="abc123xyz", + ) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_submit_overload_1(self, async_client: AsyncKernel) -> None: - invocation = await async_client.agents.auth.invocations.submit( - invocation_id="invocation_id", - field_values={ - "email": "user@example.com", - "password": "********", - }, - ) + with pytest.warns(DeprecationWarning): + invocation = await async_client.agents.auth.invocations.submit( + invocation_id="invocation_id", + field_values={ + "email": "user@example.com", + "password": "********", + }, + ) + assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_submit_overload_1(self, async_client: AsyncKernel) -> None: - response = await async_client.agents.auth.invocations.with_raw_response.submit( - invocation_id="invocation_id", - field_values={ - "email": "user@example.com", - "password": "********", - }, - ) + with pytest.warns(DeprecationWarning): + response = await async_client.agents.auth.invocations.with_raw_response.submit( + invocation_id="invocation_id", + field_values={ + "email": "user@example.com", + "password": "********", + }, + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -469,49 +521,54 @@ async def test_raw_response_submit_overload_1(self, async_client: AsyncKernel) - @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_submit_overload_1(self, async_client: AsyncKernel) -> None: - async with async_client.agents.auth.invocations.with_streaming_response.submit( - invocation_id="invocation_id", - field_values={ - "email": "user@example.com", - "password": "********", - }, - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - invocation = await response.parse() - assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) + with pytest.warns(DeprecationWarning): + async with async_client.agents.auth.invocations.with_streaming_response.submit( + invocation_id="invocation_id", + field_values={ + "email": "user@example.com", + "password": "********", + }, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + invocation = await response.parse() + assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) assert cast(Any, response.is_closed) is True @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_path_params_submit_overload_1(self, async_client: AsyncKernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): - await async_client.agents.auth.invocations.with_raw_response.submit( - invocation_id="", - field_values={ - "email": "user@example.com", - "password": "********", - }, - ) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): + await async_client.agents.auth.invocations.with_raw_response.submit( + invocation_id="", + field_values={ + "email": "user@example.com", + "password": "********", + }, + ) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_submit_overload_2(self, async_client: AsyncKernel) -> None: - invocation = await async_client.agents.auth.invocations.submit( - invocation_id="invocation_id", - sso_button="xpath=//button[contains(text(), 'Continue with Google')]", - ) + with pytest.warns(DeprecationWarning): + invocation = await async_client.agents.auth.invocations.submit( + invocation_id="invocation_id", + sso_button="xpath=//button[contains(text(), 'Continue with Google')]", + ) + assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_submit_overload_2(self, async_client: AsyncKernel) -> None: - response = await async_client.agents.auth.invocations.with_raw_response.submit( - invocation_id="invocation_id", - sso_button="xpath=//button[contains(text(), 'Continue with Google')]", - ) + with pytest.warns(DeprecationWarning): + response = await async_client.agents.auth.invocations.with_raw_response.submit( + invocation_id="invocation_id", + sso_button="xpath=//button[contains(text(), 'Continue with Google')]", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -521,43 +578,48 @@ async def test_raw_response_submit_overload_2(self, async_client: AsyncKernel) - @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_submit_overload_2(self, async_client: AsyncKernel) -> None: - async with async_client.agents.auth.invocations.with_streaming_response.submit( - invocation_id="invocation_id", - sso_button="xpath=//button[contains(text(), 'Continue with Google')]", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + async with async_client.agents.auth.invocations.with_streaming_response.submit( + invocation_id="invocation_id", + sso_button="xpath=//button[contains(text(), 'Continue with Google')]", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - invocation = await response.parse() - assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) + invocation = await response.parse() + assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) assert cast(Any, response.is_closed) is True @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_path_params_submit_overload_2(self, async_client: AsyncKernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): - await async_client.agents.auth.invocations.with_raw_response.submit( - invocation_id="", - sso_button="xpath=//button[contains(text(), 'Continue with Google')]", - ) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): + await async_client.agents.auth.invocations.with_raw_response.submit( + invocation_id="", + sso_button="xpath=//button[contains(text(), 'Continue with Google')]", + ) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_submit_overload_3(self, async_client: AsyncKernel) -> None: - invocation = await async_client.agents.auth.invocations.submit( - invocation_id="invocation_id", - selected_mfa_type="sms", - ) + with pytest.warns(DeprecationWarning): + invocation = await async_client.agents.auth.invocations.submit( + invocation_id="invocation_id", + selected_mfa_type="sms", + ) + assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_submit_overload_3(self, async_client: AsyncKernel) -> None: - response = await async_client.agents.auth.invocations.with_raw_response.submit( - invocation_id="invocation_id", - selected_mfa_type="sms", - ) + with pytest.warns(DeprecationWarning): + response = await async_client.agents.auth.invocations.with_raw_response.submit( + invocation_id="invocation_id", + selected_mfa_type="sms", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -567,23 +629,25 @@ async def test_raw_response_submit_overload_3(self, async_client: AsyncKernel) - @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_submit_overload_3(self, async_client: AsyncKernel) -> None: - async with async_client.agents.auth.invocations.with_streaming_response.submit( - invocation_id="invocation_id", - selected_mfa_type="sms", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + async with async_client.agents.auth.invocations.with_streaming_response.submit( + invocation_id="invocation_id", + selected_mfa_type="sms", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - invocation = await response.parse() - assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) + invocation = await response.parse() + assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) assert cast(Any, response.is_closed) is True @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_path_params_submit_overload_3(self, async_client: AsyncKernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): - await async_client.agents.auth.invocations.with_raw_response.submit( - invocation_id="", - selected_mfa_type="sms", - ) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): + await async_client.agents.auth.invocations.with_raw_response.submit( + invocation_id="", + selected_mfa_type="sms", + ) diff --git a/tests/api_resources/agents/test_auth.py b/tests/api_resources/agents/test_auth.py index 9855ef85..c64d77d4 100644 --- a/tests/api_resources/agents/test_auth.py +++ b/tests/api_resources/agents/test_auth.py @@ -12,6 +12,8 @@ from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination from kernel.types.agents import AuthAgent +# pyright: reportDeprecated=false + base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -21,32 +23,37 @@ class TestAuth: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_create(self, client: Kernel) -> None: - auth = client.agents.auth.create( - domain="netflix.com", - profile_name="user-123", - ) + with pytest.warns(DeprecationWarning): + auth = client.agents.auth.create( + domain="netflix.com", + profile_name="user-123", + ) + assert_matches_type(AuthAgent, auth, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_create_with_all_params(self, client: Kernel) -> None: - auth = client.agents.auth.create( - domain="netflix.com", - profile_name="user-123", - allowed_domains=["login.netflix.com", "auth.netflix.com"], - credential_name="my-netflix-login", - login_url="https://netflix.com/login", - proxy={"proxy_id": "proxy_id"}, - ) + with pytest.warns(DeprecationWarning): + auth = client.agents.auth.create( + domain="netflix.com", + profile_name="user-123", + allowed_domains=["login.netflix.com", "auth.netflix.com"], + credential_name="my-netflix-login", + login_url="https://netflix.com/login", + proxy={"proxy_id": "proxy_id"}, + ) + assert_matches_type(AuthAgent, auth, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_create(self, client: Kernel) -> None: - response = client.agents.auth.with_raw_response.create( - domain="netflix.com", - profile_name="user-123", - ) + with pytest.warns(DeprecationWarning): + response = client.agents.auth.with_raw_response.create( + domain="netflix.com", + profile_name="user-123", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -56,32 +63,36 @@ def test_raw_response_create(self, client: Kernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_create(self, client: Kernel) -> None: - with client.agents.auth.with_streaming_response.create( - domain="netflix.com", - profile_name="user-123", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + with client.agents.auth.with_streaming_response.create( + domain="netflix.com", + profile_name="user-123", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - auth = response.parse() - assert_matches_type(AuthAgent, auth, path=["response"]) + auth = response.parse() + assert_matches_type(AuthAgent, auth, 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: Kernel) -> None: - auth = client.agents.auth.retrieve( - "id", - ) + with pytest.warns(DeprecationWarning): + auth = client.agents.auth.retrieve( + "id", + ) + assert_matches_type(AuthAgent, auth, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_retrieve(self, client: Kernel) -> None: - response = client.agents.auth.with_raw_response.retrieve( - "id", - ) + with pytest.warns(DeprecationWarning): + response = client.agents.auth.with_raw_response.retrieve( + "id", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -91,46 +102,53 @@ def test_raw_response_retrieve(self, client: Kernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_retrieve(self, client: Kernel) -> None: - with client.agents.auth.with_streaming_response.retrieve( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + with client.agents.auth.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - auth = response.parse() - assert_matches_type(AuthAgent, auth, path=["response"]) + auth = response.parse() + assert_matches_type(AuthAgent, auth, 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: Kernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - client.agents.auth.with_raw_response.retrieve( - "", - ) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.agents.auth.with_raw_response.retrieve( + "", + ) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_list(self, client: Kernel) -> None: - auth = client.agents.auth.list() + with pytest.warns(DeprecationWarning): + auth = client.agents.auth.list() + assert_matches_type(SyncOffsetPagination[AuthAgent], auth, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_list_with_all_params(self, client: Kernel) -> None: - auth = client.agents.auth.list( - domain="domain", - limit=100, - offset=0, - profile_name="profile_name", - ) + with pytest.warns(DeprecationWarning): + auth = client.agents.auth.list( + domain="domain", + limit=100, + offset=0, + profile_name="profile_name", + ) + assert_matches_type(SyncOffsetPagination[AuthAgent], auth, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_list(self, client: Kernel) -> None: - response = client.agents.auth.with_raw_response.list() + with pytest.warns(DeprecationWarning): + response = client.agents.auth.with_raw_response.list() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -140,29 +158,33 @@ def test_raw_response_list(self, client: Kernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_list(self, client: Kernel) -> None: - with client.agents.auth.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + with client.agents.auth.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - auth = response.parse() - assert_matches_type(SyncOffsetPagination[AuthAgent], auth, path=["response"]) + auth = response.parse() + assert_matches_type(SyncOffsetPagination[AuthAgent], auth, 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: Kernel) -> None: - auth = client.agents.auth.delete( - "id", - ) + with pytest.warns(DeprecationWarning): + auth = client.agents.auth.delete( + "id", + ) + assert auth is None @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_raw_response_delete(self, client: Kernel) -> None: - response = client.agents.auth.with_raw_response.delete( - "id", - ) + with pytest.warns(DeprecationWarning): + response = client.agents.auth.with_raw_response.delete( + "id", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -172,24 +194,26 @@ def test_raw_response_delete(self, client: Kernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_streaming_response_delete(self, client: Kernel) -> None: - with client.agents.auth.with_streaming_response.delete( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + with client.agents.auth.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - auth = response.parse() - assert auth is None + auth = response.parse() + assert auth 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: Kernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - client.agents.auth.with_raw_response.delete( - "", - ) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.agents.auth.with_raw_response.delete( + "", + ) class TestAsyncAuth: @@ -200,32 +224,37 @@ class TestAsyncAuth: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_create(self, async_client: AsyncKernel) -> None: - auth = await async_client.agents.auth.create( - domain="netflix.com", - profile_name="user-123", - ) + with pytest.warns(DeprecationWarning): + auth = await async_client.agents.auth.create( + domain="netflix.com", + profile_name="user-123", + ) + assert_matches_type(AuthAgent, auth, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: - auth = await async_client.agents.auth.create( - domain="netflix.com", - profile_name="user-123", - allowed_domains=["login.netflix.com", "auth.netflix.com"], - credential_name="my-netflix-login", - login_url="https://netflix.com/login", - proxy={"proxy_id": "proxy_id"}, - ) + with pytest.warns(DeprecationWarning): + auth = await async_client.agents.auth.create( + domain="netflix.com", + profile_name="user-123", + allowed_domains=["login.netflix.com", "auth.netflix.com"], + credential_name="my-netflix-login", + login_url="https://netflix.com/login", + proxy={"proxy_id": "proxy_id"}, + ) + assert_matches_type(AuthAgent, auth, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_create(self, async_client: AsyncKernel) -> None: - response = await async_client.agents.auth.with_raw_response.create( - domain="netflix.com", - profile_name="user-123", - ) + with pytest.warns(DeprecationWarning): + response = await async_client.agents.auth.with_raw_response.create( + domain="netflix.com", + profile_name="user-123", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -235,32 +264,36 @@ async def test_raw_response_create(self, async_client: AsyncKernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: - async with async_client.agents.auth.with_streaming_response.create( - domain="netflix.com", - profile_name="user-123", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + async with async_client.agents.auth.with_streaming_response.create( + domain="netflix.com", + profile_name="user-123", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - auth = await response.parse() - assert_matches_type(AuthAgent, auth, path=["response"]) + auth = await response.parse() + assert_matches_type(AuthAgent, auth, 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: AsyncKernel) -> None: - auth = await async_client.agents.auth.retrieve( - "id", - ) + with pytest.warns(DeprecationWarning): + auth = await async_client.agents.auth.retrieve( + "id", + ) + assert_matches_type(AuthAgent, auth, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: - response = await async_client.agents.auth.with_raw_response.retrieve( - "id", - ) + with pytest.warns(DeprecationWarning): + response = await async_client.agents.auth.with_raw_response.retrieve( + "id", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -270,46 +303,53 @@ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: - async with async_client.agents.auth.with_streaming_response.retrieve( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + async with async_client.agents.auth.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - auth = await response.parse() - assert_matches_type(AuthAgent, auth, path=["response"]) + auth = await response.parse() + assert_matches_type(AuthAgent, auth, 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: AsyncKernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - await async_client.agents.auth.with_raw_response.retrieve( - "", - ) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.agents.auth.with_raw_response.retrieve( + "", + ) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_list(self, async_client: AsyncKernel) -> None: - auth = await async_client.agents.auth.list() + with pytest.warns(DeprecationWarning): + auth = await async_client.agents.auth.list() + assert_matches_type(AsyncOffsetPagination[AuthAgent], auth, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: - auth = await async_client.agents.auth.list( - domain="domain", - limit=100, - offset=0, - profile_name="profile_name", - ) + with pytest.warns(DeprecationWarning): + auth = await async_client.agents.auth.list( + domain="domain", + limit=100, + offset=0, + profile_name="profile_name", + ) + assert_matches_type(AsyncOffsetPagination[AuthAgent], auth, path=["response"]) @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_list(self, async_client: AsyncKernel) -> None: - response = await async_client.agents.auth.with_raw_response.list() + with pytest.warns(DeprecationWarning): + response = await async_client.agents.auth.with_raw_response.list() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -319,29 +359,33 @@ async def test_raw_response_list(self, async_client: AsyncKernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: - async with async_client.agents.auth.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + async with async_client.agents.auth.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - auth = await response.parse() - assert_matches_type(AsyncOffsetPagination[AuthAgent], auth, path=["response"]) + auth = await response.parse() + assert_matches_type(AsyncOffsetPagination[AuthAgent], auth, 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: AsyncKernel) -> None: - auth = await async_client.agents.auth.delete( - "id", - ) + with pytest.warns(DeprecationWarning): + auth = await async_client.agents.auth.delete( + "id", + ) + assert auth is None @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: - response = await async_client.agents.auth.with_raw_response.delete( - "id", - ) + with pytest.warns(DeprecationWarning): + response = await async_client.agents.auth.with_raw_response.delete( + "id", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -351,21 +395,23 @@ async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None: - async with async_client.agents.auth.with_streaming_response.delete( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + async with async_client.agents.auth.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - auth = await response.parse() - assert auth is None + auth = await response.parse() + assert auth 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: AsyncKernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - await async_client.agents.auth.with_raw_response.delete( - "", - ) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.agents.auth.with_raw_response.delete( + "", + ) diff --git a/tests/api_resources/auth/__init__.py b/tests/api_resources/auth/__init__.py new file mode 100644 index 00000000..fd8019a9 --- /dev/null +++ b/tests/api_resources/auth/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/auth/test_connections.py b/tests/api_resources/auth/test_connections.py new file mode 100644 index 00000000..96ba0a08 --- /dev/null +++ b/tests/api_resources/auth/test_connections.py @@ -0,0 +1,715 @@ +# 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 kernel import Kernel, AsyncKernel +from tests.utils import assert_matches_type +from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination +from kernel.types.auth import ( + ManagedAuth, + LoginResponse, + SubmitFieldsResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestConnections: + 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: Kernel) -> None: + connection = client.auth.connections.create( + domain="netflix.com", + profile_name="user-123", + ) + assert_matches_type(ManagedAuth, connection, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_create_with_all_params(self, client: Kernel) -> None: + connection = client.auth.connections.create( + domain="netflix.com", + profile_name="user-123", + allowed_domains=["login.netflix.com", "auth.netflix.com"], + credential={ + "auto": True, + "name": "my-netflix-creds", + "path": "Personal/Netflix", + "provider": "my-1p", + }, + health_check_interval=3600, + login_url="https://netflix.com/login", + proxy={"proxy_id": "proxy_id"}, + ) + assert_matches_type(ManagedAuth, connection, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_create(self, client: Kernel) -> None: + response = client.auth.connections.with_raw_response.create( + domain="netflix.com", + profile_name="user-123", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + connection = response.parse() + assert_matches_type(ManagedAuth, connection, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_create(self, client: Kernel) -> None: + with client.auth.connections.with_streaming_response.create( + domain="netflix.com", + profile_name="user-123", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + connection = response.parse() + assert_matches_type(ManagedAuth, connection, 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: Kernel) -> None: + connection = client.auth.connections.retrieve( + "id", + ) + assert_matches_type(ManagedAuth, connection, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: Kernel) -> None: + response = client.auth.connections.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + connection = response.parse() + assert_matches_type(ManagedAuth, connection, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: Kernel) -> None: + with client.auth.connections.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + connection = response.parse() + assert_matches_type(ManagedAuth, connection, 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: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.auth.connections.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_list(self, client: Kernel) -> None: + connection = client.auth.connections.list() + assert_matches_type(SyncOffsetPagination[ManagedAuth], connection, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_list_with_all_params(self, client: Kernel) -> None: + connection = client.auth.connections.list( + domain="domain", + limit=100, + offset=0, + profile_name="profile_name", + ) + assert_matches_type(SyncOffsetPagination[ManagedAuth], connection, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_list(self, client: Kernel) -> None: + response = client.auth.connections.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + connection = response.parse() + assert_matches_type(SyncOffsetPagination[ManagedAuth], connection, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_list(self, client: Kernel) -> None: + with client.auth.connections.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + connection = response.parse() + assert_matches_type(SyncOffsetPagination[ManagedAuth], connection, 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: Kernel) -> None: + connection = client.auth.connections.delete( + "id", + ) + assert connection is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_delete(self, client: Kernel) -> None: + response = client.auth.connections.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + connection = response.parse() + assert connection is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_delete(self, client: Kernel) -> None: + with client.auth.connections.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + connection = response.parse() + assert connection 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: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.auth.connections.with_raw_response.delete( + "", + ) + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + def test_method_follow(self, client: Kernel) -> None: + connection_stream = client.auth.connections.follow( + "id", + ) + connection_stream.response.close() + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + def test_raw_response_follow(self, client: Kernel) -> None: + response = client.auth.connections.with_raw_response.follow( + "id", + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = response.parse() + stream.close() + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + def test_streaming_response_follow(self, client: Kernel) -> None: + with client.auth.connections.with_streaming_response.follow( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = response.parse() + stream.close() + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + def test_path_params_follow(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.auth.connections.with_raw_response.follow( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_login(self, client: Kernel) -> None: + connection = client.auth.connections.login( + id="id", + ) + assert_matches_type(LoginResponse, connection, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_login_with_all_params(self, client: Kernel) -> None: + connection = client.auth.connections.login( + id="id", + save_credential_as="my-netflix-login", + ) + assert_matches_type(LoginResponse, connection, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_login(self, client: Kernel) -> None: + response = client.auth.connections.with_raw_response.login( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + connection = response.parse() + assert_matches_type(LoginResponse, connection, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_login(self, client: Kernel) -> None: + with client.auth.connections.with_streaming_response.login( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + connection = response.parse() + assert_matches_type(LoginResponse, connection, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_login(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.auth.connections.with_raw_response.login( + id="", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_submit(self, client: Kernel) -> None: + connection = client.auth.connections.submit( + id="id", + fields={ + "email": "user@example.com", + "password": "secret", + }, + ) + assert_matches_type(SubmitFieldsResponse, connection, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_submit_with_all_params(self, client: Kernel) -> None: + connection = client.auth.connections.submit( + id="id", + fields={ + "email": "user@example.com", + "password": "secret", + }, + mfa_option_id="sms", + sso_button_selector="xpath=//button[contains(text(), 'Continue with Google')]", + ) + assert_matches_type(SubmitFieldsResponse, connection, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_submit(self, client: Kernel) -> None: + response = client.auth.connections.with_raw_response.submit( + id="id", + fields={ + "email": "user@example.com", + "password": "secret", + }, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + connection = response.parse() + assert_matches_type(SubmitFieldsResponse, connection, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_submit(self, client: Kernel) -> None: + with client.auth.connections.with_streaming_response.submit( + id="id", + fields={ + "email": "user@example.com", + "password": "secret", + }, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + connection = response.parse() + assert_matches_type(SubmitFieldsResponse, connection, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_submit(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.auth.connections.with_raw_response.submit( + id="", + fields={ + "email": "user@example.com", + "password": "secret", + }, + ) + + +class TestAsyncConnections: + 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: AsyncKernel) -> None: + connection = await async_client.auth.connections.create( + domain="netflix.com", + profile_name="user-123", + ) + assert_matches_type(ManagedAuth, connection, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: + connection = await async_client.auth.connections.create( + domain="netflix.com", + profile_name="user-123", + allowed_domains=["login.netflix.com", "auth.netflix.com"], + credential={ + "auto": True, + "name": "my-netflix-creds", + "path": "Personal/Netflix", + "provider": "my-1p", + }, + health_check_interval=3600, + login_url="https://netflix.com/login", + proxy={"proxy_id": "proxy_id"}, + ) + assert_matches_type(ManagedAuth, connection, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_create(self, async_client: AsyncKernel) -> None: + response = await async_client.auth.connections.with_raw_response.create( + domain="netflix.com", + profile_name="user-123", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + connection = await response.parse() + assert_matches_type(ManagedAuth, connection, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: + async with async_client.auth.connections.with_streaming_response.create( + domain="netflix.com", + profile_name="user-123", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + connection = await response.parse() + assert_matches_type(ManagedAuth, connection, 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: AsyncKernel) -> None: + connection = await async_client.auth.connections.retrieve( + "id", + ) + assert_matches_type(ManagedAuth, connection, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: + response = await async_client.auth.connections.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + connection = await response.parse() + assert_matches_type(ManagedAuth, connection, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: + async with async_client.auth.connections.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + connection = await response.parse() + assert_matches_type(ManagedAuth, connection, 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: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.auth.connections.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncKernel) -> None: + connection = await async_client.auth.connections.list() + assert_matches_type(AsyncOffsetPagination[ManagedAuth], connection, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: + connection = await async_client.auth.connections.list( + domain="domain", + limit=100, + offset=0, + profile_name="profile_name", + ) + assert_matches_type(AsyncOffsetPagination[ManagedAuth], connection, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncKernel) -> None: + response = await async_client.auth.connections.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + connection = await response.parse() + assert_matches_type(AsyncOffsetPagination[ManagedAuth], connection, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: + async with async_client.auth.connections.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + connection = await response.parse() + assert_matches_type(AsyncOffsetPagination[ManagedAuth], connection, 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: AsyncKernel) -> None: + connection = await async_client.auth.connections.delete( + "id", + ) + assert connection is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: + response = await async_client.auth.connections.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + connection = await response.parse() + assert connection is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None: + async with async_client.auth.connections.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + connection = await response.parse() + assert connection 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: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.auth.connections.with_raw_response.delete( + "", + ) + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + async def test_method_follow(self, async_client: AsyncKernel) -> None: + connection_stream = await async_client.auth.connections.follow( + "id", + ) + await connection_stream.response.aclose() + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + async def test_raw_response_follow(self, async_client: AsyncKernel) -> None: + response = await async_client.auth.connections.with_raw_response.follow( + "id", + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = await response.parse() + await stream.close() + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + async def test_streaming_response_follow(self, async_client: AsyncKernel) -> None: + async with async_client.auth.connections.with_streaming_response.follow( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = await response.parse() + await stream.close() + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + async def test_path_params_follow(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.auth.connections.with_raw_response.follow( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_login(self, async_client: AsyncKernel) -> None: + connection = await async_client.auth.connections.login( + id="id", + ) + assert_matches_type(LoginResponse, connection, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_login_with_all_params(self, async_client: AsyncKernel) -> None: + connection = await async_client.auth.connections.login( + id="id", + save_credential_as="my-netflix-login", + ) + assert_matches_type(LoginResponse, connection, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_login(self, async_client: AsyncKernel) -> None: + response = await async_client.auth.connections.with_raw_response.login( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + connection = await response.parse() + assert_matches_type(LoginResponse, connection, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_login(self, async_client: AsyncKernel) -> None: + async with async_client.auth.connections.with_streaming_response.login( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + connection = await response.parse() + assert_matches_type(LoginResponse, connection, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_login(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.auth.connections.with_raw_response.login( + id="", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_submit(self, async_client: AsyncKernel) -> None: + connection = await async_client.auth.connections.submit( + id="id", + fields={ + "email": "user@example.com", + "password": "secret", + }, + ) + assert_matches_type(SubmitFieldsResponse, connection, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_submit_with_all_params(self, async_client: AsyncKernel) -> None: + connection = await async_client.auth.connections.submit( + id="id", + fields={ + "email": "user@example.com", + "password": "secret", + }, + mfa_option_id="sms", + sso_button_selector="xpath=//button[contains(text(), 'Continue with Google')]", + ) + assert_matches_type(SubmitFieldsResponse, connection, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_submit(self, async_client: AsyncKernel) -> None: + response = await async_client.auth.connections.with_raw_response.submit( + id="id", + fields={ + "email": "user@example.com", + "password": "secret", + }, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + connection = await response.parse() + assert_matches_type(SubmitFieldsResponse, connection, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_submit(self, async_client: AsyncKernel) -> None: + async with async_client.auth.connections.with_streaming_response.submit( + id="id", + fields={ + "email": "user@example.com", + "password": "secret", + }, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + connection = await response.parse() + assert_matches_type(SubmitFieldsResponse, connection, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_submit(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.auth.connections.with_raw_response.submit( + id="", + fields={ + "email": "user@example.com", + "password": "secret", + }, + ) diff --git a/tests/api_resources/test_credential_providers.py b/tests/api_resources/test_credential_providers.py index 136446c0..f110523f 100644 --- a/tests/api_resources/test_credential_providers.py +++ b/tests/api_resources/test_credential_providers.py @@ -13,6 +13,7 @@ CredentialProvider, CredentialProviderTestResult, CredentialProviderListResponse, + CredentialProviderListItemsResponse, ) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -26,6 +27,7 @@ class TestCredentialProviders: def test_method_create(self, client: Kernel) -> None: credential_provider = client.credential_providers.create( token="ops_eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", + name="my-1password", provider_type="onepassword", ) assert_matches_type(CredentialProvider, credential_provider, path=["response"]) @@ -35,6 +37,7 @@ def test_method_create(self, client: Kernel) -> None: def test_method_create_with_all_params(self, client: Kernel) -> None: credential_provider = client.credential_providers.create( token="ops_eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", + name="my-1password", provider_type="onepassword", cache_ttl_seconds=300, ) @@ -45,6 +48,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: def test_raw_response_create(self, client: Kernel) -> None: response = client.credential_providers.with_raw_response.create( token="ops_eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", + name="my-1password", provider_type="onepassword", ) @@ -58,6 +62,7 @@ def test_raw_response_create(self, client: Kernel) -> None: def test_streaming_response_create(self, client: Kernel) -> None: with client.credential_providers.with_streaming_response.create( token="ops_eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", + name="my-1password", provider_type="onepassword", ) as response: assert not response.is_closed @@ -126,6 +131,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None: token="ops_eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", cache_ttl_seconds=300, enabled=True, + name="my-1password", priority=0, ) assert_matches_type(CredentialProvider, credential_provider, path=["response"]) @@ -234,6 +240,48 @@ def test_path_params_delete(self, client: Kernel) -> None: "", ) + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_list_items(self, client: Kernel) -> None: + credential_provider = client.credential_providers.list_items( + "id", + ) + assert_matches_type(CredentialProviderListItemsResponse, credential_provider, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_list_items(self, client: Kernel) -> None: + response = client.credential_providers.with_raw_response.list_items( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + credential_provider = response.parse() + assert_matches_type(CredentialProviderListItemsResponse, credential_provider, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_list_items(self, client: Kernel) -> None: + with client.credential_providers.with_streaming_response.list_items( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + credential_provider = response.parse() + assert_matches_type(CredentialProviderListItemsResponse, credential_provider, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_list_items(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.credential_providers.with_raw_response.list_items( + "", + ) + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_test(self, client: Kernel) -> None: @@ -287,6 +335,7 @@ class TestAsyncCredentialProviders: async def test_method_create(self, async_client: AsyncKernel) -> None: credential_provider = await async_client.credential_providers.create( token="ops_eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", + name="my-1password", provider_type="onepassword", ) assert_matches_type(CredentialProvider, credential_provider, path=["response"]) @@ -296,6 +345,7 @@ async def test_method_create(self, async_client: AsyncKernel) -> None: async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: credential_provider = await async_client.credential_providers.create( token="ops_eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", + name="my-1password", provider_type="onepassword", cache_ttl_seconds=300, ) @@ -306,6 +356,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> async def test_raw_response_create(self, async_client: AsyncKernel) -> None: response = await async_client.credential_providers.with_raw_response.create( token="ops_eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", + name="my-1password", provider_type="onepassword", ) @@ -319,6 +370,7 @@ async def test_raw_response_create(self, async_client: AsyncKernel) -> None: async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: async with async_client.credential_providers.with_streaming_response.create( token="ops_eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", + name="my-1password", provider_type="onepassword", ) as response: assert not response.is_closed @@ -387,6 +439,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> token="ops_eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", cache_ttl_seconds=300, enabled=True, + name="my-1password", priority=0, ) assert_matches_type(CredentialProvider, credential_provider, path=["response"]) @@ -495,6 +548,48 @@ async def test_path_params_delete(self, async_client: AsyncKernel) -> None: "", ) + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_list_items(self, async_client: AsyncKernel) -> None: + credential_provider = await async_client.credential_providers.list_items( + "id", + ) + assert_matches_type(CredentialProviderListItemsResponse, credential_provider, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_list_items(self, async_client: AsyncKernel) -> None: + response = await async_client.credential_providers.with_raw_response.list_items( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + credential_provider = await response.parse() + assert_matches_type(CredentialProviderListItemsResponse, credential_provider, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_list_items(self, async_client: AsyncKernel) -> None: + async with async_client.credential_providers.with_streaming_response.list_items( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + credential_provider = await response.parse() + assert_matches_type(CredentialProviderListItemsResponse, credential_provider, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_list_items(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.credential_providers.with_raw_response.list_items( + "", + ) + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_test(self, async_client: AsyncKernel) -> None: From 2d8ce650228180dc96f3c40bdb264ca380c5d51b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 20:52:05 +0000 Subject: [PATCH 284/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index f81bf992..8305d4ab 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.31.0" + ".": "0.31.1" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 898d5dfd..e00a5130 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.31.0" +version = "0.31.1" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 4d628238..a0977c94 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.31.0" # x-release-please-version +__version__ = "0.31.1" # x-release-please-version From 9da43ac4d66c8e6e76e82b1084ee525ee165e513 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 7 Feb 2026 18:15:10 +0000 Subject: [PATCH 285/448] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 29643219..0edbaa27 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 108 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-3fbe762c99e8a120c426ac22bc1fa257c9127d631b12a38a6440a37f52935543.yml openapi_spec_hash: 5a190df210ed90b20a71c5061ff43917 -config_hash: 38c9b3b355025daf9bb643040e4af94e +config_hash: 3b1fbbb6bda0dac7e8b42e155cd7da56 From 5ed41437ce5f376e3c376f422eb4cfeb964bfb19 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 7 Feb 2026 18:15:44 +0000 Subject: [PATCH 286/448] feat(auth): add reauth circuit breaker logic --- .stats.yml | 4 ++-- src/kernel/types/agents/auth_agent.py | 3 +++ src/kernel/types/auth/managed_auth.py | 3 +++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 0edbaa27..ad62343f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 108 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-3fbe762c99e8a120c426ac22bc1fa257c9127d631b12a38a6440a37f52935543.yml -openapi_spec_hash: 5a190df210ed90b20a71c5061ff43917 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-f967d3024897a6125d5d18c4577dbb2cc22d742d487e6a43165198685f992379.yml +openapi_spec_hash: e1c40ef0aee3a79168eb9cc854a9e403 config_hash: 3b1fbbb6bda0dac7e8b42e155cd7da56 diff --git a/src/kernel/types/agents/auth_agent.py b/src/kernel/types/agents/auth_agent.py index 851b55ce..aa5b2713 100644 --- a/src/kernel/types/agents/auth_agent.py +++ b/src/kernel/types/agents/auth_agent.py @@ -76,6 +76,9 @@ class AuthAgent(BaseModel): and login_url) """ + can_reauth_reason: Optional[str] = None + """Reason why automatic re-authentication is or is not possible""" + credential: Optional[Credential] = None """Reference to credentials for managed auth. Use one of: diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py index 849616fb..426fe9e9 100644 --- a/src/kernel/types/auth/managed_auth.py +++ b/src/kernel/types/auth/managed_auth.py @@ -108,6 +108,9 @@ class ManagedAuth(BaseModel): login_url) """ + can_reauth_reason: Optional[str] = None + """Reason why automatic re-authentication is or is not possible""" + credential: Optional[Credential] = None """Reference to credentials for managed auth. Use one of: From d11b1e2a5b71bd36da1d06d7dc8ed4edd31a3b3f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 7 Feb 2026 18:20:06 +0000 Subject: [PATCH 287/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 8305d4ab..f04d0896 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.31.1" + ".": "0.32.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index e00a5130..93afc320 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.31.1" +version = "0.32.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index a0977c94..0247998a 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.31.1" # x-release-please-version +__version__ = "0.32.0" # x-release-please-version From 404b723a8d9ccb22f9272266f055a009713b3e14 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 7 Feb 2026 20:23:22 +0000 Subject: [PATCH 288/448] feat: Browser API endpoint grouping --- .stats.yml | 4 +- src/kernel/resources/auth/connections.py | 56 +++++++++---------- src/kernel/types/agents/auth_agent.py | 4 +- .../types/auth/connection_create_params.py | 4 +- src/kernel/types/auth/login_response.py | 2 +- src/kernel/types/auth/managed_auth.py | 8 +-- 6 files changed, 39 insertions(+), 39 deletions(-) diff --git a/.stats.yml b/.stats.yml index ad62343f..796d854d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 108 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-f967d3024897a6125d5d18c4577dbb2cc22d742d487e6a43165198685f992379.yml -openapi_spec_hash: e1c40ef0aee3a79168eb9cc854a9e403 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-848817f2b20afb49a652952c814b99e27a94090e0770465e9a87748d27e227a7.yml +openapi_spec_hash: 91efb805e45cdd4c73cd8b0950bef019 config_hash: 3b1fbbb6bda0dac7e8b42e155cd7da56 diff --git a/src/kernel/resources/auth/connections.py b/src/kernel/resources/auth/connections.py index 0ae31538..7030b9d1 100644 --- a/src/kernel/resources/auth/connections.py +++ b/src/kernel/resources/auth/connections.py @@ -70,10 +70,10 @@ def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ManagedAuth: - """Creates managed authentication for a profile and domain combination. + """Creates an auth connection for a profile and domain combination. Returns 409 - Conflict if managed auth already exists for the given profile and domain. + Conflict if an auth connection already exists for the given profile and domain. Args: domain: Domain for authentication @@ -99,7 +99,7 @@ def create( - Ping Identity: _.pingone.com, _.pingidentity.com credential: - Reference to credentials for managed auth. Use one of: + Reference to credentials for the auth connection. Use one of: - { name } for Kernel credentials - { provider, path } for external provider item @@ -153,10 +153,10 @@ def retrieve( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ManagedAuth: - """Retrieve managed auth by its ID. + """Retrieve an auth connection by its ID. - Includes current flow state if a login is in - progress. + Includes current flow state if a login is + in progress. Args: extra_headers: Send extra headers @@ -192,7 +192,7 @@ def list( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SyncOffsetPagination[ManagedAuth]: """ - List managed auths with optional filters for profile_name and domain. + List auth connections with optional filters for profile_name and domain. Args: domain: Filter by domain @@ -243,11 +243,11 @@ def delete( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: - """Deletes managed auth and terminates its workflow. + """Deletes an auth connection and terminates its workflow. This will: - - Delete the managed auth record + - Delete the auth connection record - Terminate the Temporal workflow - Cancel any in-progress login flows @@ -323,10 +323,10 @@ def login( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> LoginResponse: - """Starts a login flow for the managed auth. + """Starts a login flow for the auth connection. - Returns immediately with a hosted URL - for the user to complete authentication, or triggers automatic re-auth if + Returns immediately with a hosted + URL for the user to complete authentication, or triggers automatic re-auth if credentials are stored. Args: @@ -369,8 +369,8 @@ def submit( ) -> SubmitFieldsResponse: """Submits field values for the login form. - Poll the managed auth to track progress - and get results. + Poll the auth connection to track + progress and get results. Args: fields: Map of field name to value @@ -443,10 +443,10 @@ async def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ManagedAuth: - """Creates managed authentication for a profile and domain combination. + """Creates an auth connection for a profile and domain combination. Returns 409 - Conflict if managed auth already exists for the given profile and domain. + Conflict if an auth connection already exists for the given profile and domain. Args: domain: Domain for authentication @@ -472,7 +472,7 @@ async def create( - Ping Identity: _.pingone.com, _.pingidentity.com credential: - Reference to credentials for managed auth. Use one of: + Reference to credentials for the auth connection. Use one of: - { name } for Kernel credentials - { provider, path } for external provider item @@ -526,10 +526,10 @@ async def retrieve( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ManagedAuth: - """Retrieve managed auth by its ID. + """Retrieve an auth connection by its ID. - Includes current flow state if a login is in - progress. + Includes current flow state if a login is + in progress. Args: extra_headers: Send extra headers @@ -565,7 +565,7 @@ def list( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncPaginator[ManagedAuth, AsyncOffsetPagination[ManagedAuth]]: """ - List managed auths with optional filters for profile_name and domain. + List auth connections with optional filters for profile_name and domain. Args: domain: Filter by domain @@ -616,11 +616,11 @@ async def delete( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: - """Deletes managed auth and terminates its workflow. + """Deletes an auth connection and terminates its workflow. This will: - - Delete the managed auth record + - Delete the auth connection record - Terminate the Temporal workflow - Cancel any in-progress login flows @@ -696,10 +696,10 @@ async def login( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> LoginResponse: - """Starts a login flow for the managed auth. + """Starts a login flow for the auth connection. - Returns immediately with a hosted URL - for the user to complete authentication, or triggers automatic re-auth if + Returns immediately with a hosted + URL for the user to complete authentication, or triggers automatic re-auth if credentials are stored. Args: @@ -742,8 +742,8 @@ async def submit( ) -> SubmitFieldsResponse: """Submits field values for the login form. - Poll the managed auth to track progress - and get results. + Poll the auth connection to track + progress and get results. Args: fields: Map of field name to value diff --git a/src/kernel/types/agents/auth_agent.py b/src/kernel/types/agents/auth_agent.py index aa5b2713..98e11589 100644 --- a/src/kernel/types/agents/auth_agent.py +++ b/src/kernel/types/agents/auth_agent.py @@ -10,7 +10,7 @@ class Credential(BaseModel): - """Reference to credentials for managed auth. + """Reference to credentials for the auth connection. Use one of: - { name } for Kernel credentials @@ -80,7 +80,7 @@ class AuthAgent(BaseModel): """Reason why automatic re-authentication is or is not possible""" credential: Optional[Credential] = None - """Reference to credentials for managed auth. Use one of: + """Reference to credentials for the auth connection. Use one of: - { name } for Kernel credentials - { provider, path } for external provider item diff --git a/src/kernel/types/auth/connection_create_params.py b/src/kernel/types/auth/connection_create_params.py index 9282f48d..2c31f603 100644 --- a/src/kernel/types/auth/connection_create_params.py +++ b/src/kernel/types/auth/connection_create_params.py @@ -38,7 +38,7 @@ class ConnectionCreateParams(TypedDict, total=False): """ credential: Credential - """Reference to credentials for managed auth. Use one of: + """Reference to credentials for the auth connection. Use one of: - { name } for Kernel credentials - { provider, path } for external provider item @@ -61,7 +61,7 @@ class ConnectionCreateParams(TypedDict, total=False): class Credential(TypedDict, total=False): - """Reference to credentials for managed auth. + """Reference to credentials for the auth connection. Use one of: - { name } for Kernel credentials diff --git a/src/kernel/types/auth/login_response.py b/src/kernel/types/auth/login_response.py index 178c3005..f5aed291 100644 --- a/src/kernel/types/auth/login_response.py +++ b/src/kernel/types/auth/login_response.py @@ -13,7 +13,7 @@ class LoginResponse(BaseModel): """Response from starting a login flow""" id: str - """Managed auth ID""" + """Auth connection ID""" flow_expires_at: datetime """When the login flow expires""" diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py index 426fe9e9..3fb2286f 100644 --- a/src/kernel/types/auth/managed_auth.py +++ b/src/kernel/types/auth/managed_auth.py @@ -11,7 +11,7 @@ class Credential(BaseModel): - """Reference to credentials for managed auth. + """Reference to credentials for the auth connection. Use one of: - { name } for Kernel credentials @@ -70,13 +70,13 @@ class ManagedAuth(BaseModel): """ id: str - """Unique identifier for the managed auth""" + """Unique identifier for the auth connection""" domain: str """Target domain for authentication""" profile_name: str - """Name of the profile associated with this managed auth""" + """Name of the profile associated with this auth connection""" status: Literal["AUTHENTICATED", "NEEDS_AUTH"] """Current authentication status of the managed profile""" @@ -112,7 +112,7 @@ class ManagedAuth(BaseModel): """Reason why automatic re-authentication is or is not possible""" credential: Optional[Credential] = None - """Reference to credentials for managed auth. Use one of: + """Reference to credentials for the auth connection. Use one of: - { name } for Kernel credentials - { provider, path } for external provider item From a1c5d4965178e3cab3b3b04b2861cab84988c552 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 19:24:41 +0000 Subject: [PATCH 289/448] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 796d854d..15c374f0 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 108 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-848817f2b20afb49a652952c814b99e27a94090e0770465e9a87748d27e227a7.yml -openapi_spec_hash: 91efb805e45cdd4c73cd8b0950bef019 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-13e82ae9e725e2c3ca19da7248a7a9c8696a0dfe088654cf26aea07c76d6567a.yml +openapi_spec_hash: 6d4151a6066a8474bc56923299aec18a config_hash: 3b1fbbb6bda0dac7e8b42e155cd7da56 From ce489da30d983d27314a204b0c14d3c138b5ab84 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 01:48:33 +0000 Subject: [PATCH 290/448] =?UTF-8?q?refactor(api):=20remove=20deprecated=20?= =?UTF-8?q?agent-auth=20endpoints=20from=20stainless.=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .stats.yml | 4 +- api.md | 40 - src/kernel/_client.py | 38 - src/kernel/resources/__init__.py | 14 - src/kernel/resources/agents/__init__.py | 33 - src/kernel/resources/agents/agents.py | 102 --- src/kernel/resources/agents/auth/__init__.py | 33 - src/kernel/resources/agents/auth/auth.py | 634 ---------------- .../resources/agents/auth/invocations.py | 682 ------------------ src/kernel/types/agents/__init__.py | 13 - .../agents/agent_auth_invocation_response.py | 101 --- .../agents/agent_auth_submit_response.py | 14 - src/kernel/types/agents/auth/__init__.py | 8 - .../agents/auth/invocation_create_params.py | 19 - .../agents/auth/invocation_exchange_params.py | 12 - .../auth/invocation_exchange_response.py | 15 - .../agents/auth/invocation_submit_params.py | 28 - src/kernel/types/agents/auth_agent.py | 108 --- .../auth_agent_invocation_create_response.py | 31 - src/kernel/types/agents/auth_create_params.py | 63 -- src/kernel/types/agents/auth_list_params.py | 21 - src/kernel/types/agents/discovered_field.py | 36 - .../types/auth/connection_follow_response.py | 32 +- src/kernel/types/auth/managed_auth.py | 31 +- tests/api_resources/agents/__init__.py | 1 - tests/api_resources/agents/auth/__init__.py | 1 - .../agents/auth/test_invocations.py | 653 ----------------- tests/api_resources/agents/test_auth.py | 417 ----------- 28 files changed, 61 insertions(+), 3123 deletions(-) delete mode 100644 src/kernel/resources/agents/__init__.py delete mode 100644 src/kernel/resources/agents/agents.py delete mode 100644 src/kernel/resources/agents/auth/__init__.py delete mode 100644 src/kernel/resources/agents/auth/auth.py delete mode 100644 src/kernel/resources/agents/auth/invocations.py delete mode 100644 src/kernel/types/agents/__init__.py delete mode 100644 src/kernel/types/agents/agent_auth_invocation_response.py delete mode 100644 src/kernel/types/agents/agent_auth_submit_response.py delete mode 100644 src/kernel/types/agents/auth/__init__.py delete mode 100644 src/kernel/types/agents/auth/invocation_create_params.py delete mode 100644 src/kernel/types/agents/auth/invocation_exchange_params.py delete mode 100644 src/kernel/types/agents/auth/invocation_exchange_response.py delete mode 100644 src/kernel/types/agents/auth/invocation_submit_params.py delete mode 100644 src/kernel/types/agents/auth_agent.py delete mode 100644 src/kernel/types/agents/auth_agent_invocation_create_response.py delete mode 100644 src/kernel/types/agents/auth_create_params.py delete mode 100644 src/kernel/types/agents/auth_list_params.py delete mode 100644 src/kernel/types/agents/discovered_field.py delete mode 100644 tests/api_resources/agents/__init__.py delete mode 100644 tests/api_resources/agents/auth/__init__.py delete mode 100644 tests/api_resources/agents/auth/test_invocations.py delete mode 100644 tests/api_resources/agents/test_auth.py diff --git a/.stats.yml b/.stats.yml index 15c374f0..207d8996 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 108 +configured_endpoints: 100 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-13e82ae9e725e2c3ca19da7248a7a9c8696a0dfe088654cf26aea07c76d6567a.yml openapi_spec_hash: 6d4151a6066a8474bc56923299aec18a -config_hash: 3b1fbbb6bda0dac7e8b42e155cd7da56 +config_hash: 82f0a04081a3ab7111d3a9c68cd3ff2b diff --git a/api.md b/api.md index cb50de40..87d0de32 100644 --- a/api.md +++ b/api.md @@ -318,46 +318,6 @@ Methods: - client.browser_pools.flush(id_or_name) -> None - client.browser_pools.release(id_or_name, \*\*params) -> None -# Agents - -## Auth - -Types: - -```python -from kernel.types.agents import ( - AgentAuthInvocationResponse, - AgentAuthSubmitResponse, - AuthAgent, - AuthAgentCreateRequest, - AuthAgentInvocationCreateRequest, - AuthAgentInvocationCreateResponse, - DiscoveredField, -) -``` - -Methods: - -- client.agents.auth.create(\*\*params) -> AuthAgent -- client.agents.auth.retrieve(id) -> AuthAgent -- client.agents.auth.list(\*\*params) -> SyncOffsetPagination[AuthAgent] -- client.agents.auth.delete(id) -> None - -### Invocations - -Types: - -```python -from kernel.types.agents.auth import InvocationExchangeResponse -``` - -Methods: - -- client.agents.auth.invocations.create(\*\*params) -> AuthAgentInvocationCreateResponse -- client.agents.auth.invocations.retrieve(invocation_id) -> AgentAuthInvocationResponse -- client.agents.auth.invocations.exchange(invocation_id, \*\*params) -> InvocationExchangeResponse -- client.agents.auth.invocations.submit(invocation_id, \*\*params) -> AgentAuthSubmitResponse - # Credentials Types: diff --git a/src/kernel/_client.py b/src/kernel/_client.py index 07c3b682..df7fd255 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -34,7 +34,6 @@ from .resources import ( apps, auth, - agents, proxies, browsers, profiles, @@ -53,7 +52,6 @@ from .resources.credentials import CredentialsResource, AsyncCredentialsResource from .resources.deployments import DeploymentsResource, AsyncDeploymentsResource from .resources.invocations import InvocationsResource, AsyncInvocationsResource - from .resources.agents.agents import AgentsResource, AsyncAgentsResource from .resources.browser_pools import BrowserPoolsResource, AsyncBrowserPoolsResource from .resources.browsers.browsers import BrowsersResource, AsyncBrowsersResource from .resources.credential_providers import CredentialProvidersResource, AsyncCredentialProvidersResource @@ -209,12 +207,6 @@ def browser_pools(self) -> BrowserPoolsResource: return BrowserPoolsResource(self) - @cached_property - def agents(self) -> AgentsResource: - from .resources.agents import AgentsResource - - return AgentsResource(self) - @cached_property def credentials(self) -> CredentialsResource: from .resources.credentials import CredentialsResource @@ -475,12 +467,6 @@ def browser_pools(self) -> AsyncBrowserPoolsResource: return AsyncBrowserPoolsResource(self) - @cached_property - def agents(self) -> AsyncAgentsResource: - from .resources.agents import AsyncAgentsResource - - return AsyncAgentsResource(self) - @cached_property def credentials(self) -> AsyncCredentialsResource: from .resources.credentials import AsyncCredentialsResource @@ -668,12 +654,6 @@ def browser_pools(self) -> browser_pools.BrowserPoolsResourceWithRawResponse: return BrowserPoolsResourceWithRawResponse(self._client.browser_pools) - @cached_property - def agents(self) -> agents.AgentsResourceWithRawResponse: - from .resources.agents import AgentsResourceWithRawResponse - - return AgentsResourceWithRawResponse(self._client.agents) - @cached_property def credentials(self) -> credentials.CredentialsResourceWithRawResponse: from .resources.credentials import CredentialsResourceWithRawResponse @@ -747,12 +727,6 @@ def browser_pools(self) -> browser_pools.AsyncBrowserPoolsResourceWithRawRespons return AsyncBrowserPoolsResourceWithRawResponse(self._client.browser_pools) - @cached_property - def agents(self) -> agents.AsyncAgentsResourceWithRawResponse: - from .resources.agents import AsyncAgentsResourceWithRawResponse - - return AsyncAgentsResourceWithRawResponse(self._client.agents) - @cached_property def credentials(self) -> credentials.AsyncCredentialsResourceWithRawResponse: from .resources.credentials import AsyncCredentialsResourceWithRawResponse @@ -826,12 +800,6 @@ def browser_pools(self) -> browser_pools.BrowserPoolsResourceWithStreamingRespon return BrowserPoolsResourceWithStreamingResponse(self._client.browser_pools) - @cached_property - def agents(self) -> agents.AgentsResourceWithStreamingResponse: - from .resources.agents import AgentsResourceWithStreamingResponse - - return AgentsResourceWithStreamingResponse(self._client.agents) - @cached_property def credentials(self) -> credentials.CredentialsResourceWithStreamingResponse: from .resources.credentials import CredentialsResourceWithStreamingResponse @@ -905,12 +873,6 @@ def browser_pools(self) -> browser_pools.AsyncBrowserPoolsResourceWithStreamingR return AsyncBrowserPoolsResourceWithStreamingResponse(self._client.browser_pools) - @cached_property - def agents(self) -> agents.AsyncAgentsResourceWithStreamingResponse: - from .resources.agents import AsyncAgentsResourceWithStreamingResponse - - return AsyncAgentsResourceWithStreamingResponse(self._client.agents) - @cached_property def credentials(self) -> credentials.AsyncCredentialsResourceWithStreamingResponse: from .resources.credentials import AsyncCredentialsResourceWithStreamingResponse diff --git a/src/kernel/resources/__init__.py b/src/kernel/resources/__init__.py index 31a325b2..4896e79b 100644 --- a/src/kernel/resources/__init__.py +++ b/src/kernel/resources/__init__.py @@ -16,14 +16,6 @@ AuthResourceWithStreamingResponse, AsyncAuthResourceWithStreamingResponse, ) -from .agents import ( - AgentsResource, - AsyncAgentsResource, - AgentsResourceWithRawResponse, - AsyncAgentsResourceWithRawResponse, - AgentsResourceWithStreamingResponse, - AsyncAgentsResourceWithStreamingResponse, -) from .proxies import ( ProxiesResource, AsyncProxiesResource, @@ -152,12 +144,6 @@ "AsyncBrowserPoolsResourceWithRawResponse", "BrowserPoolsResourceWithStreamingResponse", "AsyncBrowserPoolsResourceWithStreamingResponse", - "AgentsResource", - "AsyncAgentsResource", - "AgentsResourceWithRawResponse", - "AsyncAgentsResourceWithRawResponse", - "AgentsResourceWithStreamingResponse", - "AsyncAgentsResourceWithStreamingResponse", "CredentialsResource", "AsyncCredentialsResource", "CredentialsResourceWithRawResponse", diff --git a/src/kernel/resources/agents/__init__.py b/src/kernel/resources/agents/__init__.py deleted file mode 100644 index cb159eb7..00000000 --- a/src/kernel/resources/agents/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from .auth import ( - AuthResource, - AsyncAuthResource, - AuthResourceWithRawResponse, - AsyncAuthResourceWithRawResponse, - AuthResourceWithStreamingResponse, - AsyncAuthResourceWithStreamingResponse, -) -from .agents import ( - AgentsResource, - AsyncAgentsResource, - AgentsResourceWithRawResponse, - AsyncAgentsResourceWithRawResponse, - AgentsResourceWithStreamingResponse, - AsyncAgentsResourceWithStreamingResponse, -) - -__all__ = [ - "AuthResource", - "AsyncAuthResource", - "AuthResourceWithRawResponse", - "AsyncAuthResourceWithRawResponse", - "AuthResourceWithStreamingResponse", - "AsyncAuthResourceWithStreamingResponse", - "AgentsResource", - "AsyncAgentsResource", - "AgentsResourceWithRawResponse", - "AsyncAgentsResourceWithRawResponse", - "AgentsResourceWithStreamingResponse", - "AsyncAgentsResourceWithStreamingResponse", -] diff --git a/src/kernel/resources/agents/agents.py b/src/kernel/resources/agents/agents.py deleted file mode 100644 index 6999bd58..00000000 --- a/src/kernel/resources/agents/agents.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 ..._compat import cached_property -from .auth.auth import ( - AuthResource, - AsyncAuthResource, - AuthResourceWithRawResponse, - AsyncAuthResourceWithRawResponse, - AuthResourceWithStreamingResponse, - AsyncAuthResourceWithStreamingResponse, -) -from ..._resource import SyncAPIResource, AsyncAPIResource - -__all__ = ["AgentsResource", "AsyncAgentsResource"] - - -class AgentsResource(SyncAPIResource): - @cached_property - def auth(self) -> AuthResource: - return AuthResource(self._client) - - @cached_property - def with_raw_response(self) -> AgentsResourceWithRawResponse: - """ - 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/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers - """ - return AgentsResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AgentsResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response - """ - return AgentsResourceWithStreamingResponse(self) - - -class AsyncAgentsResource(AsyncAPIResource): - @cached_property - def auth(self) -> AsyncAuthResource: - return AsyncAuthResource(self._client) - - @cached_property - def with_raw_response(self) -> AsyncAgentsResourceWithRawResponse: - """ - 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/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers - """ - return AsyncAgentsResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncAgentsResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response - """ - return AsyncAgentsResourceWithStreamingResponse(self) - - -class AgentsResourceWithRawResponse: - def __init__(self, agents: AgentsResource) -> None: - self._agents = agents - - @cached_property - def auth(self) -> AuthResourceWithRawResponse: - return AuthResourceWithRawResponse(self._agents.auth) - - -class AsyncAgentsResourceWithRawResponse: - def __init__(self, agents: AsyncAgentsResource) -> None: - self._agents = agents - - @cached_property - def auth(self) -> AsyncAuthResourceWithRawResponse: - return AsyncAuthResourceWithRawResponse(self._agents.auth) - - -class AgentsResourceWithStreamingResponse: - def __init__(self, agents: AgentsResource) -> None: - self._agents = agents - - @cached_property - def auth(self) -> AuthResourceWithStreamingResponse: - return AuthResourceWithStreamingResponse(self._agents.auth) - - -class AsyncAgentsResourceWithStreamingResponse: - def __init__(self, agents: AsyncAgentsResource) -> None: - self._agents = agents - - @cached_property - def auth(self) -> AsyncAuthResourceWithStreamingResponse: - return AsyncAuthResourceWithStreamingResponse(self._agents.auth) diff --git a/src/kernel/resources/agents/auth/__init__.py b/src/kernel/resources/agents/auth/__init__.py deleted file mode 100644 index 61305493..00000000 --- a/src/kernel/resources/agents/auth/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from .auth import ( - AuthResource, - AsyncAuthResource, - AuthResourceWithRawResponse, - AsyncAuthResourceWithRawResponse, - AuthResourceWithStreamingResponse, - AsyncAuthResourceWithStreamingResponse, -) -from .invocations import ( - InvocationsResource, - AsyncInvocationsResource, - InvocationsResourceWithRawResponse, - AsyncInvocationsResourceWithRawResponse, - InvocationsResourceWithStreamingResponse, - AsyncInvocationsResourceWithStreamingResponse, -) - -__all__ = [ - "InvocationsResource", - "AsyncInvocationsResource", - "InvocationsResourceWithRawResponse", - "AsyncInvocationsResourceWithRawResponse", - "InvocationsResourceWithStreamingResponse", - "AsyncInvocationsResourceWithStreamingResponse", - "AuthResource", - "AsyncAuthResource", - "AuthResourceWithRawResponse", - "AsyncAuthResourceWithRawResponse", - "AuthResourceWithStreamingResponse", - "AsyncAuthResourceWithStreamingResponse", -] diff --git a/src/kernel/resources/agents/auth/auth.py b/src/kernel/resources/agents/auth/auth.py deleted file mode 100644 index 05e83c50..00000000 --- a/src/kernel/resources/agents/auth/auth.py +++ /dev/null @@ -1,634 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import typing_extensions - -import httpx - -from ...._types import Body, Omit, Query, Headers, NoneType, NotGiven, SequenceNotStr, omit, not_given -from ...._utils import maybe_transform, async_maybe_transform -from ...._compat import cached_property -from .invocations import ( - InvocationsResource, - AsyncInvocationsResource, - InvocationsResourceWithRawResponse, - AsyncInvocationsResourceWithRawResponse, - InvocationsResourceWithStreamingResponse, - AsyncInvocationsResourceWithStreamingResponse, -) -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 ....pagination import SyncOffsetPagination, AsyncOffsetPagination -from ...._base_client import AsyncPaginator, make_request_options -from ....types.agents import auth_list_params, auth_create_params -from ....types.agents.auth_agent import AuthAgent - -__all__ = ["AuthResource", "AsyncAuthResource"] - - -class AuthResource(SyncAPIResource): - @cached_property - def invocations(self) -> InvocationsResource: - return InvocationsResource(self._client) - - @cached_property - def with_raw_response(self) -> AuthResourceWithRawResponse: - """ - 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/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers - """ - return AuthResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AuthResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response - """ - return AuthResourceWithStreamingResponse(self) - - @typing_extensions.deprecated("deprecated") - def create( - self, - *, - domain: str, - profile_name: str, - allowed_domains: SequenceNotStr[str] | Omit = omit, - credential_name: str | Omit = omit, - login_url: str | Omit = omit, - proxy: auth_create_params.Proxy | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> AuthAgent: - """ - **Deprecated: Use POST /auth/connections instead.** Creates a new auth agent for - the specified domain and profile combination, or returns an existing one if it - already exists. This is idempotent - calling with the same domain and profile - will return the same agent. Does NOT start an invocation - use POST - /agents/auth/invocations to start an auth flow. - - Args: - domain: Domain for authentication - - profile_name: Name of the profile to use for this auth agent - - allowed_domains: Additional domains that are valid for this auth agent's authentication flow - (besides the primary domain). Useful when login pages redirect to different - domains. - - The following SSO/OAuth provider domains are automatically allowed by default - and do not need to be specified: - - - Google: accounts.google.com - - Microsoft/Azure AD: login.microsoftonline.com, login.live.com - - Okta: _.okta.com, _.oktapreview.com - - Auth0: _.auth0.com, _.us.auth0.com, _.eu.auth0.com, _.au.auth0.com - - Apple: appleid.apple.com - - GitHub: github.com - - Facebook/Meta: www.facebook.com - - LinkedIn: www.linkedin.com - - Amazon Cognito: \\**.amazoncognito.com - - OneLogin: \\**.onelogin.com - - Ping Identity: _.pingone.com, _.pingidentity.com - - credential_name: Optional name of an existing credential to use for this auth agent. If provided, - the credential will be linked to the agent and its values will be used to - auto-fill the login form on invocation. - - login_url: Optional login page URL. If provided, will be stored on the agent and used to - skip discovery in future invocations. - - proxy: Optional proxy configuration - - 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( - "/agents/auth", - body=maybe_transform( - { - "domain": domain, - "profile_name": profile_name, - "allowed_domains": allowed_domains, - "credential_name": credential_name, - "login_url": login_url, - "proxy": proxy, - }, - auth_create_params.AuthCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AuthAgent, - ) - - @typing_extensions.deprecated("deprecated") - def retrieve( - self, - id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> AuthAgent: - """ - **Deprecated: Use GET /auth/connections/{id} instead.** Retrieve an auth agent - by its ID. Returns the current authentication status of the managed profile. - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - return self._get( - f"/agents/auth/{id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AuthAgent, - ) - - @typing_extensions.deprecated("deprecated") - def list( - self, - *, - domain: str | Omit = omit, - limit: int | Omit = omit, - offset: int | Omit = omit, - profile_name: str | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SyncOffsetPagination[AuthAgent]: - """ - **Deprecated: Use GET /auth/connections instead.** List auth agents with - optional filters for profile_name and domain. - - Args: - domain: Filter by domain - - limit: Maximum number of results to return - - offset: Number of results to skip - - profile_name: Filter by profile name - - 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_api_list( - "/agents/auth", - page=SyncOffsetPagination[AuthAgent], - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=maybe_transform( - { - "domain": domain, - "limit": limit, - "offset": offset, - "profile_name": profile_name, - }, - auth_list_params.AuthListParams, - ), - ), - model=AuthAgent, - ) - - @typing_extensions.deprecated("deprecated") - def delete( - self, - id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> None: - """ - **Deprecated: Use DELETE /auth/connections/{id} instead.** Deletes an auth agent - and terminates its workflow. This will: - - - Soft delete the auth agent record - - Gracefully terminate the agent's Temporal workflow - - Cancel any in-progress invocations - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return self._delete( - f"/agents/auth/{id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=NoneType, - ) - - -class AsyncAuthResource(AsyncAPIResource): - @cached_property - def invocations(self) -> AsyncInvocationsResource: - return AsyncInvocationsResource(self._client) - - @cached_property - def with_raw_response(self) -> AsyncAuthResourceWithRawResponse: - """ - 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/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers - """ - return AsyncAuthResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncAuthResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response - """ - return AsyncAuthResourceWithStreamingResponse(self) - - @typing_extensions.deprecated("deprecated") - async def create( - self, - *, - domain: str, - profile_name: str, - allowed_domains: SequenceNotStr[str] | Omit = omit, - credential_name: str | Omit = omit, - login_url: str | Omit = omit, - proxy: auth_create_params.Proxy | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> AuthAgent: - """ - **Deprecated: Use POST /auth/connections instead.** Creates a new auth agent for - the specified domain and profile combination, or returns an existing one if it - already exists. This is idempotent - calling with the same domain and profile - will return the same agent. Does NOT start an invocation - use POST - /agents/auth/invocations to start an auth flow. - - Args: - domain: Domain for authentication - - profile_name: Name of the profile to use for this auth agent - - allowed_domains: Additional domains that are valid for this auth agent's authentication flow - (besides the primary domain). Useful when login pages redirect to different - domains. - - The following SSO/OAuth provider domains are automatically allowed by default - and do not need to be specified: - - - Google: accounts.google.com - - Microsoft/Azure AD: login.microsoftonline.com, login.live.com - - Okta: _.okta.com, _.oktapreview.com - - Auth0: _.auth0.com, _.us.auth0.com, _.eu.auth0.com, _.au.auth0.com - - Apple: appleid.apple.com - - GitHub: github.com - - Facebook/Meta: www.facebook.com - - LinkedIn: www.linkedin.com - - Amazon Cognito: \\**.amazoncognito.com - - OneLogin: \\**.onelogin.com - - Ping Identity: _.pingone.com, _.pingidentity.com - - credential_name: Optional name of an existing credential to use for this auth agent. If provided, - the credential will be linked to the agent and its values will be used to - auto-fill the login form on invocation. - - login_url: Optional login page URL. If provided, will be stored on the agent and used to - skip discovery in future invocations. - - proxy: Optional proxy configuration - - 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( - "/agents/auth", - body=await async_maybe_transform( - { - "domain": domain, - "profile_name": profile_name, - "allowed_domains": allowed_domains, - "credential_name": credential_name, - "login_url": login_url, - "proxy": proxy, - }, - auth_create_params.AuthCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AuthAgent, - ) - - @typing_extensions.deprecated("deprecated") - async def retrieve( - self, - id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> AuthAgent: - """ - **Deprecated: Use GET /auth/connections/{id} instead.** Retrieve an auth agent - by its ID. Returns the current authentication status of the managed profile. - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - return await self._get( - f"/agents/auth/{id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AuthAgent, - ) - - @typing_extensions.deprecated("deprecated") - def list( - self, - *, - domain: str | Omit = omit, - limit: int | Omit = omit, - offset: int | Omit = omit, - profile_name: str | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> AsyncPaginator[AuthAgent, AsyncOffsetPagination[AuthAgent]]: - """ - **Deprecated: Use GET /auth/connections instead.** List auth agents with - optional filters for profile_name and domain. - - Args: - domain: Filter by domain - - limit: Maximum number of results to return - - offset: Number of results to skip - - profile_name: Filter by profile name - - 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_api_list( - "/agents/auth", - page=AsyncOffsetPagination[AuthAgent], - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=maybe_transform( - { - "domain": domain, - "limit": limit, - "offset": offset, - "profile_name": profile_name, - }, - auth_list_params.AuthListParams, - ), - ), - model=AuthAgent, - ) - - @typing_extensions.deprecated("deprecated") - async def delete( - self, - id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> None: - """ - **Deprecated: Use DELETE /auth/connections/{id} instead.** Deletes an auth agent - and terminates its workflow. This will: - - - Soft delete the auth agent record - - Gracefully terminate the agent's Temporal workflow - - Cancel any in-progress invocations - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return await self._delete( - f"/agents/auth/{id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=NoneType, - ) - - -class AuthResourceWithRawResponse: - def __init__(self, auth: AuthResource) -> None: - self._auth = auth - - self.create = ( # pyright: ignore[reportDeprecated] - to_raw_response_wrapper( - auth.create, # pyright: ignore[reportDeprecated], - ) - ) - self.retrieve = ( # pyright: ignore[reportDeprecated] - to_raw_response_wrapper( - auth.retrieve, # pyright: ignore[reportDeprecated], - ) - ) - self.list = ( # pyright: ignore[reportDeprecated] - to_raw_response_wrapper( - auth.list, # pyright: ignore[reportDeprecated], - ) - ) - self.delete = ( # pyright: ignore[reportDeprecated] - to_raw_response_wrapper( - auth.delete, # pyright: ignore[reportDeprecated], - ) - ) - - @cached_property - def invocations(self) -> InvocationsResourceWithRawResponse: - return InvocationsResourceWithRawResponse(self._auth.invocations) - - -class AsyncAuthResourceWithRawResponse: - def __init__(self, auth: AsyncAuthResource) -> None: - self._auth = auth - - self.create = ( # pyright: ignore[reportDeprecated] - async_to_raw_response_wrapper( - auth.create, # pyright: ignore[reportDeprecated], - ) - ) - self.retrieve = ( # pyright: ignore[reportDeprecated] - async_to_raw_response_wrapper( - auth.retrieve, # pyright: ignore[reportDeprecated], - ) - ) - self.list = ( # pyright: ignore[reportDeprecated] - async_to_raw_response_wrapper( - auth.list, # pyright: ignore[reportDeprecated], - ) - ) - self.delete = ( # pyright: ignore[reportDeprecated] - async_to_raw_response_wrapper( - auth.delete, # pyright: ignore[reportDeprecated], - ) - ) - - @cached_property - def invocations(self) -> AsyncInvocationsResourceWithRawResponse: - return AsyncInvocationsResourceWithRawResponse(self._auth.invocations) - - -class AuthResourceWithStreamingResponse: - def __init__(self, auth: AuthResource) -> None: - self._auth = auth - - self.create = ( # pyright: ignore[reportDeprecated] - to_streamed_response_wrapper( - auth.create, # pyright: ignore[reportDeprecated], - ) - ) - self.retrieve = ( # pyright: ignore[reportDeprecated] - to_streamed_response_wrapper( - auth.retrieve, # pyright: ignore[reportDeprecated], - ) - ) - self.list = ( # pyright: ignore[reportDeprecated] - to_streamed_response_wrapper( - auth.list, # pyright: ignore[reportDeprecated], - ) - ) - self.delete = ( # pyright: ignore[reportDeprecated] - to_streamed_response_wrapper( - auth.delete, # pyright: ignore[reportDeprecated], - ) - ) - - @cached_property - def invocations(self) -> InvocationsResourceWithStreamingResponse: - return InvocationsResourceWithStreamingResponse(self._auth.invocations) - - -class AsyncAuthResourceWithStreamingResponse: - def __init__(self, auth: AsyncAuthResource) -> None: - self._auth = auth - - self.create = ( # pyright: ignore[reportDeprecated] - async_to_streamed_response_wrapper( - auth.create, # pyright: ignore[reportDeprecated], - ) - ) - self.retrieve = ( # pyright: ignore[reportDeprecated] - async_to_streamed_response_wrapper( - auth.retrieve, # pyright: ignore[reportDeprecated], - ) - ) - self.list = ( # pyright: ignore[reportDeprecated] - async_to_streamed_response_wrapper( - auth.list, # pyright: ignore[reportDeprecated], - ) - ) - self.delete = ( # pyright: ignore[reportDeprecated] - async_to_streamed_response_wrapper( - auth.delete, # pyright: ignore[reportDeprecated], - ) - ) - - @cached_property - def invocations(self) -> AsyncInvocationsResourceWithStreamingResponse: - return AsyncInvocationsResourceWithStreamingResponse(self._auth.invocations) diff --git a/src/kernel/resources/agents/auth/invocations.py b/src/kernel/resources/agents/auth/invocations.py deleted file mode 100644 index 617afddd..00000000 --- a/src/kernel/resources/agents/auth/invocations.py +++ /dev/null @@ -1,682 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import typing_extensions -from typing import Dict -from typing_extensions import Literal, overload - -import httpx - -from ...._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from ...._utils import required_args, 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.agents.auth import invocation_create_params, invocation_submit_params, invocation_exchange_params -from ....types.agents.agent_auth_submit_response import AgentAuthSubmitResponse -from ....types.agents.agent_auth_invocation_response import AgentAuthInvocationResponse -from ....types.agents.auth.invocation_exchange_response import InvocationExchangeResponse -from ....types.agents.auth_agent_invocation_create_response import AuthAgentInvocationCreateResponse - -__all__ = ["InvocationsResource", "AsyncInvocationsResource"] - - -class InvocationsResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> InvocationsResourceWithRawResponse: - """ - 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/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers - """ - return InvocationsResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> InvocationsResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response - """ - return InvocationsResourceWithStreamingResponse(self) - - @typing_extensions.deprecated("deprecated") - def create( - self, - *, - auth_agent_id: str, - save_credential_as: str | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> AuthAgentInvocationCreateResponse: - """ - **Deprecated: Use POST /auth/connections/{id}/login instead.** Creates a new - authentication invocation for the specified auth agent. This starts the auth - flow and returns a hosted URL for the user to complete authentication. - - Args: - auth_agent_id: ID of the auth agent to create an invocation for - - save_credential_as: If provided, saves the submitted credentials under this name upon successful - login. The credential will be linked to the auth agent for automatic - re-authentication. - - 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( - "/agents/auth/invocations", - body=maybe_transform( - { - "auth_agent_id": auth_agent_id, - "save_credential_as": save_credential_as, - }, - invocation_create_params.InvocationCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AuthAgentInvocationCreateResponse, - ) - - @typing_extensions.deprecated("deprecated") - def retrieve( - self, - invocation_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, - ) -> AgentAuthInvocationResponse: - """ - **Deprecated: Use GET /auth/connections/{id} instead.** Returns invocation - details including status, app_name, and domain. Supports both API key and JWT - (from exchange endpoint) authentication. - - 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 invocation_id: - raise ValueError(f"Expected a non-empty value for `invocation_id` but received {invocation_id!r}") - return self._get( - f"/agents/auth/invocations/{invocation_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AgentAuthInvocationResponse, - ) - - @typing_extensions.deprecated("deprecated") - def exchange( - self, - invocation_id: str, - *, - code: 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, - ) -> InvocationExchangeResponse: - """ - **Deprecated: Use POST /auth/connections/{id}/exchange instead.** Validates the - handoff code and returns a JWT token for subsequent requests. No authentication - required (the handoff code serves as the credential). - - Args: - code: Handoff code from start endpoint - - 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 invocation_id: - raise ValueError(f"Expected a non-empty value for `invocation_id` but received {invocation_id!r}") - return self._post( - f"/agents/auth/invocations/{invocation_id}/exchange", - body=maybe_transform({"code": code}, invocation_exchange_params.InvocationExchangeParams), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=InvocationExchangeResponse, - ) - - @typing_extensions.deprecated("deprecated") - @overload - def submit( - self, - invocation_id: str, - *, - field_values: Dict[str, 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, - ) -> AgentAuthSubmitResponse: - """ - **Deprecated: Use POST /auth/connections/{id}/submit instead.** Submits field - values for the discovered login form. Returns immediately after submission is - accepted. Poll the invocation endpoint to track progress and get results. - - Args: - field_values: Values for the discovered login fields - - 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 - """ - ... - - @typing_extensions.deprecated("deprecated") - @overload - def submit( - self, - invocation_id: str, - *, - sso_button: 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, - ) -> AgentAuthSubmitResponse: - """ - **Deprecated: Use POST /auth/connections/{id}/submit instead.** Submits field - values for the discovered login form. Returns immediately after submission is - accepted. Poll the invocation endpoint to track progress and get results. - - Args: - sso_button: Selector of SSO button to click - - 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 - """ - ... - - @typing_extensions.deprecated("deprecated") - @overload - def submit( - self, - invocation_id: str, - *, - selected_mfa_type: Literal["sms", "call", "email", "totp", "push", "password"], - # Use the following arguments if you need to pass additional parameters to the API that aren't available via 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, - ) -> AgentAuthSubmitResponse: - """ - **Deprecated: Use POST /auth/connections/{id}/submit instead.** Submits field - values for the discovered login form. Returns immediately after submission is - accepted. Poll the invocation endpoint to track progress and get results. - - Args: - selected_mfa_type: The MFA delivery method type (includes password for auth method selection pages) - - 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 - """ - ... - - @typing_extensions.deprecated("deprecated") - @required_args(["field_values"], ["sso_button"], ["selected_mfa_type"]) - def submit( - self, - invocation_id: str, - *, - field_values: Dict[str, str] | Omit = omit, - sso_button: str | Omit = omit, - selected_mfa_type: Literal["sms", "call", "email", "totp", "push", "password"] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> AgentAuthSubmitResponse: - if not invocation_id: - raise ValueError(f"Expected a non-empty value for `invocation_id` but received {invocation_id!r}") - return self._post( - f"/agents/auth/invocations/{invocation_id}/submit", - body=maybe_transform( - { - "field_values": field_values, - "sso_button": sso_button, - "selected_mfa_type": selected_mfa_type, - }, - invocation_submit_params.InvocationSubmitParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AgentAuthSubmitResponse, - ) - - -class AsyncInvocationsResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncInvocationsResourceWithRawResponse: - """ - 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/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers - """ - return AsyncInvocationsResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncInvocationsResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response - """ - return AsyncInvocationsResourceWithStreamingResponse(self) - - @typing_extensions.deprecated("deprecated") - async def create( - self, - *, - auth_agent_id: str, - save_credential_as: str | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> AuthAgentInvocationCreateResponse: - """ - **Deprecated: Use POST /auth/connections/{id}/login instead.** Creates a new - authentication invocation for the specified auth agent. This starts the auth - flow and returns a hosted URL for the user to complete authentication. - - Args: - auth_agent_id: ID of the auth agent to create an invocation for - - save_credential_as: If provided, saves the submitted credentials under this name upon successful - login. The credential will be linked to the auth agent for automatic - re-authentication. - - 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( - "/agents/auth/invocations", - body=await async_maybe_transform( - { - "auth_agent_id": auth_agent_id, - "save_credential_as": save_credential_as, - }, - invocation_create_params.InvocationCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AuthAgentInvocationCreateResponse, - ) - - @typing_extensions.deprecated("deprecated") - async def retrieve( - self, - invocation_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, - ) -> AgentAuthInvocationResponse: - """ - **Deprecated: Use GET /auth/connections/{id} instead.** Returns invocation - details including status, app_name, and domain. Supports both API key and JWT - (from exchange endpoint) authentication. - - 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 invocation_id: - raise ValueError(f"Expected a non-empty value for `invocation_id` but received {invocation_id!r}") - return await self._get( - f"/agents/auth/invocations/{invocation_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AgentAuthInvocationResponse, - ) - - @typing_extensions.deprecated("deprecated") - async def exchange( - self, - invocation_id: str, - *, - code: 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, - ) -> InvocationExchangeResponse: - """ - **Deprecated: Use POST /auth/connections/{id}/exchange instead.** Validates the - handoff code and returns a JWT token for subsequent requests. No authentication - required (the handoff code serves as the credential). - - Args: - code: Handoff code from start endpoint - - 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 invocation_id: - raise ValueError(f"Expected a non-empty value for `invocation_id` but received {invocation_id!r}") - return await self._post( - f"/agents/auth/invocations/{invocation_id}/exchange", - body=await async_maybe_transform({"code": code}, invocation_exchange_params.InvocationExchangeParams), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=InvocationExchangeResponse, - ) - - @typing_extensions.deprecated("deprecated") - @overload - async def submit( - self, - invocation_id: str, - *, - field_values: Dict[str, 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, - ) -> AgentAuthSubmitResponse: - """ - **Deprecated: Use POST /auth/connections/{id}/submit instead.** Submits field - values for the discovered login form. Returns immediately after submission is - accepted. Poll the invocation endpoint to track progress and get results. - - Args: - field_values: Values for the discovered login fields - - 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 - """ - ... - - @typing_extensions.deprecated("deprecated") - @overload - async def submit( - self, - invocation_id: str, - *, - sso_button: 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, - ) -> AgentAuthSubmitResponse: - """ - **Deprecated: Use POST /auth/connections/{id}/submit instead.** Submits field - values for the discovered login form. Returns immediately after submission is - accepted. Poll the invocation endpoint to track progress and get results. - - Args: - sso_button: Selector of SSO button to click - - 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 - """ - ... - - @typing_extensions.deprecated("deprecated") - @overload - async def submit( - self, - invocation_id: str, - *, - selected_mfa_type: Literal["sms", "call", "email", "totp", "push", "password"], - # Use the following arguments if you need to pass additional parameters to the API that aren't available via 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, - ) -> AgentAuthSubmitResponse: - """ - **Deprecated: Use POST /auth/connections/{id}/submit instead.** Submits field - values for the discovered login form. Returns immediately after submission is - accepted. Poll the invocation endpoint to track progress and get results. - - Args: - selected_mfa_type: The MFA delivery method type (includes password for auth method selection pages) - - 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 - """ - ... - - @typing_extensions.deprecated("deprecated") - @required_args(["field_values"], ["sso_button"], ["selected_mfa_type"]) - async def submit( - self, - invocation_id: str, - *, - field_values: Dict[str, str] | Omit = omit, - sso_button: str | Omit = omit, - selected_mfa_type: Literal["sms", "call", "email", "totp", "push", "password"] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> AgentAuthSubmitResponse: - if not invocation_id: - raise ValueError(f"Expected a non-empty value for `invocation_id` but received {invocation_id!r}") - return await self._post( - f"/agents/auth/invocations/{invocation_id}/submit", - body=await async_maybe_transform( - { - "field_values": field_values, - "sso_button": sso_button, - "selected_mfa_type": selected_mfa_type, - }, - invocation_submit_params.InvocationSubmitParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AgentAuthSubmitResponse, - ) - - -class InvocationsResourceWithRawResponse: - def __init__(self, invocations: InvocationsResource) -> None: - self._invocations = invocations - - self.create = ( # pyright: ignore[reportDeprecated] - to_raw_response_wrapper( - invocations.create, # pyright: ignore[reportDeprecated], - ) - ) - self.retrieve = ( # pyright: ignore[reportDeprecated] - to_raw_response_wrapper( - invocations.retrieve, # pyright: ignore[reportDeprecated], - ) - ) - self.exchange = ( # pyright: ignore[reportDeprecated] - to_raw_response_wrapper( - invocations.exchange, # pyright: ignore[reportDeprecated], - ) - ) - self.submit = ( # pyright: ignore[reportDeprecated] - to_raw_response_wrapper( - invocations.submit, # pyright: ignore[reportDeprecated], - ) - ) - - -class AsyncInvocationsResourceWithRawResponse: - def __init__(self, invocations: AsyncInvocationsResource) -> None: - self._invocations = invocations - - self.create = ( # pyright: ignore[reportDeprecated] - async_to_raw_response_wrapper( - invocations.create, # pyright: ignore[reportDeprecated], - ) - ) - self.retrieve = ( # pyright: ignore[reportDeprecated] - async_to_raw_response_wrapper( - invocations.retrieve, # pyright: ignore[reportDeprecated], - ) - ) - self.exchange = ( # pyright: ignore[reportDeprecated] - async_to_raw_response_wrapper( - invocations.exchange, # pyright: ignore[reportDeprecated], - ) - ) - self.submit = ( # pyright: ignore[reportDeprecated] - async_to_raw_response_wrapper( - invocations.submit, # pyright: ignore[reportDeprecated], - ) - ) - - -class InvocationsResourceWithStreamingResponse: - def __init__(self, invocations: InvocationsResource) -> None: - self._invocations = invocations - - self.create = ( # pyright: ignore[reportDeprecated] - to_streamed_response_wrapper( - invocations.create, # pyright: ignore[reportDeprecated], - ) - ) - self.retrieve = ( # pyright: ignore[reportDeprecated] - to_streamed_response_wrapper( - invocations.retrieve, # pyright: ignore[reportDeprecated], - ) - ) - self.exchange = ( # pyright: ignore[reportDeprecated] - to_streamed_response_wrapper( - invocations.exchange, # pyright: ignore[reportDeprecated], - ) - ) - self.submit = ( # pyright: ignore[reportDeprecated] - to_streamed_response_wrapper( - invocations.submit, # pyright: ignore[reportDeprecated], - ) - ) - - -class AsyncInvocationsResourceWithStreamingResponse: - def __init__(self, invocations: AsyncInvocationsResource) -> None: - self._invocations = invocations - - self.create = ( # pyright: ignore[reportDeprecated] - async_to_streamed_response_wrapper( - invocations.create, # pyright: ignore[reportDeprecated], - ) - ) - self.retrieve = ( # pyright: ignore[reportDeprecated] - async_to_streamed_response_wrapper( - invocations.retrieve, # pyright: ignore[reportDeprecated], - ) - ) - self.exchange = ( # pyright: ignore[reportDeprecated] - async_to_streamed_response_wrapper( - invocations.exchange, # pyright: ignore[reportDeprecated], - ) - ) - self.submit = ( # pyright: ignore[reportDeprecated] - async_to_streamed_response_wrapper( - invocations.submit, # pyright: ignore[reportDeprecated], - ) - ) diff --git a/src/kernel/types/agents/__init__.py b/src/kernel/types/agents/__init__.py deleted file mode 100644 index 2ecdef65..00000000 --- a/src/kernel/types/agents/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from .auth_agent import AuthAgent as AuthAgent -from .auth_list_params import AuthListParams as AuthListParams -from .discovered_field import DiscoveredField as DiscoveredField -from .auth_create_params import AuthCreateParams as AuthCreateParams -from .agent_auth_submit_response import AgentAuthSubmitResponse as AgentAuthSubmitResponse -from .agent_auth_invocation_response import AgentAuthInvocationResponse as AgentAuthInvocationResponse -from .auth_agent_invocation_create_response import ( - AuthAgentInvocationCreateResponse as AuthAgentInvocationCreateResponse, -) diff --git a/src/kernel/types/agents/agent_auth_invocation_response.py b/src/kernel/types/agents/agent_auth_invocation_response.py deleted file mode 100644 index fa755492..00000000 --- a/src/kernel/types/agents/agent_auth_invocation_response.py +++ /dev/null @@ -1,101 +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 typing_extensions import Literal - -from ..._models import BaseModel -from .discovered_field import DiscoveredField - -__all__ = ["AgentAuthInvocationResponse", "MfaOption", "PendingSSOButton"] - - -class MfaOption(BaseModel): - """An MFA method option for verification""" - - label: str - """The visible option text""" - - type: Literal["sms", "call", "email", "totp", "push", "password"] - """ - The MFA delivery method type (includes password for auth method selection pages) - """ - - description: Optional[str] = None - """Additional instructions from the site""" - - target: Optional[str] = None - """The masked destination (phone/email) if shown""" - - -class PendingSSOButton(BaseModel): - """An SSO button for signing in with an external identity provider""" - - label: str - """Visible button text""" - - provider: str - """Identity provider name""" - - selector: str - """XPath selector for the button""" - - -class AgentAuthInvocationResponse(BaseModel): - """Response from get invocation endpoint""" - - app_name: str - """App name (org name at time of invocation creation)""" - - domain: str - """Domain for authentication""" - - expires_at: datetime - """When the handoff code expires""" - - status: Literal["IN_PROGRESS", "SUCCESS", "EXPIRED", "CANCELED", "FAILED"] - """Invocation status""" - - step: Literal[ - "initialized", "discovering", "awaiting_input", "awaiting_external_action", "submitting", "completed", "expired" - ] - """Current step in the invocation workflow""" - - type: Literal["login", "reauth"] - """The session type: - - - login: User-initiated authentication - - reauth: System-triggered re-authentication (via health check) - """ - - error_message: Optional[str] = None - """Error message explaining why the invocation failed (present when status=FAILED)""" - - external_action_message: Optional[str] = None - """ - Instructions for user when external action is required (present when - step=awaiting_external_action) - """ - - live_view_url: Optional[str] = None - """Browser live view URL for debugging the invocation""" - - mfa_options: Optional[List[MfaOption]] = None - """ - MFA method options to choose from (present when step=awaiting_input and MFA - selection is required) - """ - - pending_fields: Optional[List[DiscoveredField]] = None - """Fields currently awaiting input (present when step=awaiting_input)""" - - pending_sso_buttons: Optional[List[PendingSSOButton]] = None - """SSO buttons available on the page (present when step=awaiting_input)""" - - sso_provider: Optional[str] = None - """SSO provider being used for authentication (e.g., google, github, microsoft)""" - - submitted_fields: Optional[List[str]] = None - """ - Names of fields that have been submitted (present when step=submitting or later) - """ diff --git a/src/kernel/types/agents/agent_auth_submit_response.py b/src/kernel/types/agents/agent_auth_submit_response.py deleted file mode 100644 index 8cb0df14..00000000 --- a/src/kernel/types/agents/agent_auth_submit_response.py +++ /dev/null @@ -1,14 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from ..._models import BaseModel - -__all__ = ["AgentAuthSubmitResponse"] - - -class AgentAuthSubmitResponse(BaseModel): - """ - Response from submit endpoint - returns immediately after submission is accepted - """ - - accepted: bool - """Whether the submission was accepted for processing""" diff --git a/src/kernel/types/agents/auth/__init__.py b/src/kernel/types/agents/auth/__init__.py deleted file mode 100644 index 41e8ba8c..00000000 --- a/src/kernel/types/agents/auth/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from .invocation_create_params import InvocationCreateParams as InvocationCreateParams -from .invocation_submit_params import InvocationSubmitParams as InvocationSubmitParams -from .invocation_exchange_params import InvocationExchangeParams as InvocationExchangeParams -from .invocation_exchange_response import InvocationExchangeResponse as InvocationExchangeResponse diff --git a/src/kernel/types/agents/auth/invocation_create_params.py b/src/kernel/types/agents/auth/invocation_create_params.py deleted file mode 100644 index b2727e02..00000000 --- a/src/kernel/types/agents/auth/invocation_create_params.py +++ /dev/null @@ -1,19 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Required, TypedDict - -__all__ = ["InvocationCreateParams"] - - -class InvocationCreateParams(TypedDict, total=False): - auth_agent_id: Required[str] - """ID of the auth agent to create an invocation for""" - - save_credential_as: str - """ - If provided, saves the submitted credentials under this name upon successful - login. The credential will be linked to the auth agent for automatic - re-authentication. - """ diff --git a/src/kernel/types/agents/auth/invocation_exchange_params.py b/src/kernel/types/agents/auth/invocation_exchange_params.py deleted file mode 100644 index 71e4d184..00000000 --- a/src/kernel/types/agents/auth/invocation_exchange_params.py +++ /dev/null @@ -1,12 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Required, TypedDict - -__all__ = ["InvocationExchangeParams"] - - -class InvocationExchangeParams(TypedDict, total=False): - code: Required[str] - """Handoff code from start endpoint""" diff --git a/src/kernel/types/agents/auth/invocation_exchange_response.py b/src/kernel/types/agents/auth/invocation_exchange_response.py deleted file mode 100644 index 710d9c31..00000000 --- a/src/kernel/types/agents/auth/invocation_exchange_response.py +++ /dev/null @@ -1,15 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from ...._models import BaseModel - -__all__ = ["InvocationExchangeResponse"] - - -class InvocationExchangeResponse(BaseModel): - """Response from exchange endpoint""" - - invocation_id: str - """Invocation ID""" - - jwt: str - """JWT token with invocation_id claim (30 minute TTL)""" diff --git a/src/kernel/types/agents/auth/invocation_submit_params.py b/src/kernel/types/agents/auth/invocation_submit_params.py deleted file mode 100644 index dc5ee5f6..00000000 --- a/src/kernel/types/agents/auth/invocation_submit_params.py +++ /dev/null @@ -1,28 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Dict, Union -from typing_extensions import Literal, Required, TypeAlias, TypedDict - -__all__ = ["InvocationSubmitParams", "Variant0", "Variant1", "Variant2"] - - -class Variant0(TypedDict, total=False): - field_values: Required[Dict[str, str]] - """Values for the discovered login fields""" - - -class Variant1(TypedDict, total=False): - sso_button: Required[str] - """Selector of SSO button to click""" - - -class Variant2(TypedDict, total=False): - selected_mfa_type: Required[Literal["sms", "call", "email", "totp", "push", "password"]] - """ - The MFA delivery method type (includes password for auth method selection pages) - """ - - -InvocationSubmitParams: TypeAlias = Union[Variant0, Variant1, Variant2] diff --git a/src/kernel/types/agents/auth_agent.py b/src/kernel/types/agents/auth_agent.py deleted file mode 100644 index 98e11589..00000000 --- a/src/kernel/types/agents/auth_agent.py +++ /dev/null @@ -1,108 +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 typing_extensions import Literal - -from ..._models import BaseModel - -__all__ = ["AuthAgent", "Credential"] - - -class Credential(BaseModel): - """Reference to credentials for the auth connection. - - Use one of: - - { name } for Kernel credentials - - { provider, path } for external provider item - - { provider, auto: true } for external provider domain lookup - """ - - auto: Optional[bool] = None - """If true, lookup by domain from the specified provider""" - - name: Optional[str] = None - """Kernel credential name""" - - path: Optional[str] = None - """Provider-specific path (e.g., "VaultName/ItemName" for 1Password)""" - - provider: Optional[str] = None - """External provider name (e.g., "my-1p")""" - - -class AuthAgent(BaseModel): - """ - An auth agent that manages authentication for a specific domain and profile combination - """ - - id: str - """Unique identifier for the auth agent""" - - domain: str - """Target domain for authentication""" - - profile_name: str - """Name of the profile associated with this auth agent""" - - status: Literal["AUTHENTICATED", "NEEDS_AUTH"] - """Current authentication status of the managed profile""" - - allowed_domains: Optional[List[str]] = None - """ - Additional domains that are valid for this auth agent's authentication flow - (besides the primary domain). Useful when login pages redirect to different - domains. - - The following SSO/OAuth provider domains are automatically allowed by default - and do not need to be specified: - - - Google: accounts.google.com - - Microsoft/Azure AD: login.microsoftonline.com, login.live.com - - Okta: _.okta.com, _.oktapreview.com - - Auth0: _.auth0.com, _.us.auth0.com, _.eu.auth0.com, _.au.auth0.com - - Apple: appleid.apple.com - - GitHub: github.com - - Facebook/Meta: www.facebook.com - - LinkedIn: www.linkedin.com - - Amazon Cognito: \\**.amazoncognito.com - - OneLogin: \\**.onelogin.com - - Ping Identity: _.pingone.com, _.pingidentity.com - """ - - can_reauth: Optional[bool] = None - """ - Whether automatic re-authentication is possible (has credential_id, selectors, - and login_url) - """ - - can_reauth_reason: Optional[str] = None - """Reason why automatic re-authentication is or is not possible""" - - credential: Optional[Credential] = None - """Reference to credentials for the auth connection. Use one of: - - - { name } for Kernel credentials - - { provider, path } for external provider item - - { provider, auto: true } for external provider domain lookup - """ - - credential_id: Optional[str] = None - """ - ID of the linked Kernel credential for automatic re-authentication (deprecated, - use credential) - """ - - has_selectors: Optional[bool] = None - """ - Whether this auth agent has stored selectors for deterministic re-authentication - """ - - last_auth_check_at: Optional[datetime] = None - """When the last authentication check was performed""" - - post_login_url: Optional[str] = None - """URL where the browser landed after successful login. - - Query parameters and fragments are stripped for privacy. - """ diff --git a/src/kernel/types/agents/auth_agent_invocation_create_response.py b/src/kernel/types/agents/auth_agent_invocation_create_response.py deleted file mode 100644 index fc5d8dba..00000000 --- a/src/kernel/types/agents/auth_agent_invocation_create_response.py +++ /dev/null @@ -1,31 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from datetime import datetime -from typing_extensions import Literal - -from ..._models import BaseModel - -__all__ = ["AuthAgentInvocationCreateResponse"] - - -class AuthAgentInvocationCreateResponse(BaseModel): - """Response from creating an invocation. Always returns an invocation_id.""" - - expires_at: datetime - """When the handoff code expires.""" - - handoff_code: str - """One-time code for handoff.""" - - hosted_url: str - """URL to redirect user to.""" - - invocation_id: str - """Unique identifier for the invocation.""" - - type: Literal["login", "reauth"] - """The session type: - - - login: User-initiated authentication - - reauth: System-triggered re-authentication (via health check) - """ diff --git a/src/kernel/types/agents/auth_create_params.py b/src/kernel/types/agents/auth_create_params.py deleted file mode 100644 index 613f0034..00000000 --- a/src/kernel/types/agents/auth_create_params.py +++ /dev/null @@ -1,63 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Required, TypedDict - -from ..._types import SequenceNotStr - -__all__ = ["AuthCreateParams", "Proxy"] - - -class AuthCreateParams(TypedDict, total=False): - domain: Required[str] - """Domain for authentication""" - - profile_name: Required[str] - """Name of the profile to use for this auth agent""" - - allowed_domains: SequenceNotStr[str] - """ - Additional domains that are valid for this auth agent's authentication flow - (besides the primary domain). Useful when login pages redirect to different - domains. - - The following SSO/OAuth provider domains are automatically allowed by default - and do not need to be specified: - - - Google: accounts.google.com - - Microsoft/Azure AD: login.microsoftonline.com, login.live.com - - Okta: _.okta.com, _.oktapreview.com - - Auth0: _.auth0.com, _.us.auth0.com, _.eu.auth0.com, _.au.auth0.com - - Apple: appleid.apple.com - - GitHub: github.com - - Facebook/Meta: www.facebook.com - - LinkedIn: www.linkedin.com - - Amazon Cognito: \\**.amazoncognito.com - - OneLogin: \\**.onelogin.com - - Ping Identity: _.pingone.com, _.pingidentity.com - """ - - credential_name: str - """Optional name of an existing credential to use for this auth agent. - - If provided, the credential will be linked to the agent and its values will be - used to auto-fill the login form on invocation. - """ - - login_url: str - """Optional login page URL. - - If provided, will be stored on the agent and used to skip discovery in future - invocations. - """ - - proxy: Proxy - """Optional proxy configuration""" - - -class Proxy(TypedDict, total=False): - """Optional proxy configuration""" - - proxy_id: str - """ID of the proxy to use""" diff --git a/src/kernel/types/agents/auth_list_params.py b/src/kernel/types/agents/auth_list_params.py deleted file mode 100644 index 52d53375..00000000 --- a/src/kernel/types/agents/auth_list_params.py +++ /dev/null @@ -1,21 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import TypedDict - -__all__ = ["AuthListParams"] - - -class AuthListParams(TypedDict, total=False): - domain: str - """Filter by domain""" - - limit: int - """Maximum number of results to return""" - - offset: int - """Number of results to skip""" - - profile_name: str - """Filter by profile name""" diff --git a/src/kernel/types/agents/discovered_field.py b/src/kernel/types/agents/discovered_field.py deleted file mode 100644 index 4dbf53a8..00000000 --- a/src/kernel/types/agents/discovered_field.py +++ /dev/null @@ -1,36 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional -from typing_extensions import Literal - -from ..._models import BaseModel - -__all__ = ["DiscoveredField"] - - -class DiscoveredField(BaseModel): - """A discovered form field""" - - label: str - """Field label""" - - name: str - """Field name""" - - selector: str - """CSS selector for the field""" - - type: Literal["text", "email", "password", "tel", "number", "url", "code", "totp"] - """Field type""" - - linked_mfa_type: Optional[Literal["sms", "call", "email", "totp", "push", "password"]] = None - """ - If this field is associated with an MFA option, the type of that option (e.g., - password field linked to "Enter password" option) - """ - - placeholder: Optional[str] = None - """Field placeholder""" - - required: Optional[bool] = None - """Whether field is required""" diff --git a/src/kernel/types/auth/connection_follow_response.py b/src/kernel/types/auth/connection_follow_response.py index 488ec81a..e54d5c10 100644 --- a/src/kernel/types/auth/connection_follow_response.py +++ b/src/kernel/types/auth/connection_follow_response.py @@ -8,16 +8,44 @@ from ..._models import BaseModel from ..shared.error_event import ErrorEvent from ..shared.heartbeat_event import HeartbeatEvent -from ..agents.discovered_field import DiscoveredField __all__ = [ "ConnectionFollowResponse", "ManagedAuthStateEvent", + "ManagedAuthStateEventDiscoveredField", "ManagedAuthStateEventMfaOption", "ManagedAuthStateEventPendingSSOButton", ] +class ManagedAuthStateEventDiscoveredField(BaseModel): + """A discovered form field""" + + label: str + """Field label""" + + name: str + """Field name""" + + selector: str + """CSS selector for the field""" + + type: Literal["text", "email", "password", "tel", "number", "url", "code", "totp"] + """Field type""" + + linked_mfa_type: Optional[Literal["sms", "call", "email", "totp", "push", "password"]] = None + """ + If this field is associated with an MFA option, the type of that option (e.g., + password field linked to "Enter password" option) + """ + + placeholder: Optional[str] = None + """Field placeholder""" + + required: Optional[bool] = None + """Whether field is required""" + + class ManagedAuthStateEventMfaOption(BaseModel): """An MFA method option for verification""" @@ -64,7 +92,7 @@ class ManagedAuthStateEvent(BaseModel): timestamp: datetime """Time the state was reported.""" - discovered_fields: Optional[List[DiscoveredField]] = None + discovered_fields: Optional[List[ManagedAuthStateEventDiscoveredField]] = None """Fields awaiting input (present when flow_step=AWAITING_INPUT).""" error_message: Optional[str] = None diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py index 3fb2286f..9cfc8271 100644 --- a/src/kernel/types/auth/managed_auth.py +++ b/src/kernel/types/auth/managed_auth.py @@ -5,9 +5,8 @@ from typing_extensions import Literal from ..._models import BaseModel -from ..agents.discovered_field import DiscoveredField -__all__ = ["ManagedAuth", "Credential", "MfaOption", "PendingSSOButton"] +__all__ = ["ManagedAuth", "Credential", "DiscoveredField", "MfaOption", "PendingSSOButton"] class Credential(BaseModel): @@ -32,6 +31,34 @@ class Credential(BaseModel): """External provider name (e.g., "my-1p")""" +class DiscoveredField(BaseModel): + """A discovered form field""" + + label: str + """Field label""" + + name: str + """Field name""" + + selector: str + """CSS selector for the field""" + + type: Literal["text", "email", "password", "tel", "number", "url", "code", "totp"] + """Field type""" + + linked_mfa_type: Optional[Literal["sms", "call", "email", "totp", "push", "password"]] = None + """ + If this field is associated with an MFA option, the type of that option (e.g., + password field linked to "Enter password" option) + """ + + placeholder: Optional[str] = None + """Field placeholder""" + + required: Optional[bool] = None + """Whether field is required""" + + class MfaOption(BaseModel): """An MFA method option for verification""" diff --git a/tests/api_resources/agents/__init__.py b/tests/api_resources/agents/__init__.py deleted file mode 100644 index fd8019a9..00000000 --- a/tests/api_resources/agents/__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/agents/auth/__init__.py b/tests/api_resources/agents/auth/__init__.py deleted file mode 100644 index fd8019a9..00000000 --- a/tests/api_resources/agents/auth/__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/agents/auth/test_invocations.py b/tests/api_resources/agents/auth/test_invocations.py deleted file mode 100644 index 829a6076..00000000 --- a/tests/api_resources/agents/auth/test_invocations.py +++ /dev/null @@ -1,653 +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 kernel import Kernel, AsyncKernel -from tests.utils import assert_matches_type -from kernel.types.agents import AgentAuthSubmitResponse, AgentAuthInvocationResponse, AuthAgentInvocationCreateResponse -from kernel.types.agents.auth import ( - InvocationExchangeResponse, -) - -# pyright: reportDeprecated=false - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestInvocations: - 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: Kernel) -> None: - with pytest.warns(DeprecationWarning): - invocation = client.agents.auth.invocations.create( - auth_agent_id="abc123xyz", - ) - - assert_matches_type(AuthAgentInvocationCreateResponse, invocation, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_create_with_all_params(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - invocation = client.agents.auth.invocations.create( - auth_agent_id="abc123xyz", - save_credential_as="my-netflix-login", - ) - - assert_matches_type(AuthAgentInvocationCreateResponse, invocation, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_create(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - response = client.agents.auth.invocations.with_raw_response.create( - auth_agent_id="abc123xyz", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - invocation = response.parse() - assert_matches_type(AuthAgentInvocationCreateResponse, invocation, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_create(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - with client.agents.auth.invocations.with_streaming_response.create( - auth_agent_id="abc123xyz", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - invocation = response.parse() - assert_matches_type(AuthAgentInvocationCreateResponse, invocation, 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: Kernel) -> None: - with pytest.warns(DeprecationWarning): - invocation = client.agents.auth.invocations.retrieve( - "invocation_id", - ) - - assert_matches_type(AgentAuthInvocationResponse, invocation, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_retrieve(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - response = client.agents.auth.invocations.with_raw_response.retrieve( - "invocation_id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - invocation = response.parse() - assert_matches_type(AgentAuthInvocationResponse, invocation, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_retrieve(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - with client.agents.auth.invocations.with_streaming_response.retrieve( - "invocation_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - invocation = response.parse() - assert_matches_type(AgentAuthInvocationResponse, invocation, 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: Kernel) -> None: - with pytest.warns(DeprecationWarning): - with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): - client.agents.auth.invocations.with_raw_response.retrieve( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_exchange(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - invocation = client.agents.auth.invocations.exchange( - invocation_id="invocation_id", - code="abc123xyz", - ) - - assert_matches_type(InvocationExchangeResponse, invocation, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_exchange(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - response = client.agents.auth.invocations.with_raw_response.exchange( - invocation_id="invocation_id", - code="abc123xyz", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - invocation = response.parse() - assert_matches_type(InvocationExchangeResponse, invocation, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_exchange(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - with client.agents.auth.invocations.with_streaming_response.exchange( - invocation_id="invocation_id", - code="abc123xyz", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - invocation = response.parse() - assert_matches_type(InvocationExchangeResponse, invocation, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_exchange(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): - client.agents.auth.invocations.with_raw_response.exchange( - invocation_id="", - code="abc123xyz", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_submit_overload_1(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - invocation = client.agents.auth.invocations.submit( - invocation_id="invocation_id", - field_values={ - "email": "user@example.com", - "password": "********", - }, - ) - - assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_submit_overload_1(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - response = client.agents.auth.invocations.with_raw_response.submit( - invocation_id="invocation_id", - field_values={ - "email": "user@example.com", - "password": "********", - }, - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - invocation = response.parse() - assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_submit_overload_1(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - with client.agents.auth.invocations.with_streaming_response.submit( - invocation_id="invocation_id", - field_values={ - "email": "user@example.com", - "password": "********", - }, - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - invocation = response.parse() - assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_submit_overload_1(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): - client.agents.auth.invocations.with_raw_response.submit( - invocation_id="", - field_values={ - "email": "user@example.com", - "password": "********", - }, - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_submit_overload_2(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - invocation = client.agents.auth.invocations.submit( - invocation_id="invocation_id", - sso_button="xpath=//button[contains(text(), 'Continue with Google')]", - ) - - assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_submit_overload_2(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - response = client.agents.auth.invocations.with_raw_response.submit( - invocation_id="invocation_id", - sso_button="xpath=//button[contains(text(), 'Continue with Google')]", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - invocation = response.parse() - assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_submit_overload_2(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - with client.agents.auth.invocations.with_streaming_response.submit( - invocation_id="invocation_id", - sso_button="xpath=//button[contains(text(), 'Continue with Google')]", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - invocation = response.parse() - assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_submit_overload_2(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): - client.agents.auth.invocations.with_raw_response.submit( - invocation_id="", - sso_button="xpath=//button[contains(text(), 'Continue with Google')]", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_submit_overload_3(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - invocation = client.agents.auth.invocations.submit( - invocation_id="invocation_id", - selected_mfa_type="sms", - ) - - assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_submit_overload_3(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - response = client.agents.auth.invocations.with_raw_response.submit( - invocation_id="invocation_id", - selected_mfa_type="sms", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - invocation = response.parse() - assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_submit_overload_3(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - with client.agents.auth.invocations.with_streaming_response.submit( - invocation_id="invocation_id", - selected_mfa_type="sms", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - invocation = response.parse() - assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_submit_overload_3(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): - client.agents.auth.invocations.with_raw_response.submit( - invocation_id="", - selected_mfa_type="sms", - ) - - -class TestAsyncInvocations: - 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: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - invocation = await async_client.agents.auth.invocations.create( - auth_agent_id="abc123xyz", - ) - - assert_matches_type(AuthAgentInvocationCreateResponse, invocation, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - invocation = await async_client.agents.auth.invocations.create( - auth_agent_id="abc123xyz", - save_credential_as="my-netflix-login", - ) - - assert_matches_type(AuthAgentInvocationCreateResponse, invocation, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_create(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - response = await async_client.agents.auth.invocations.with_raw_response.create( - auth_agent_id="abc123xyz", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - invocation = await response.parse() - assert_matches_type(AuthAgentInvocationCreateResponse, invocation, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - async with async_client.agents.auth.invocations.with_streaming_response.create( - auth_agent_id="abc123xyz", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - invocation = await response.parse() - assert_matches_type(AuthAgentInvocationCreateResponse, invocation, 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: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - invocation = await async_client.agents.auth.invocations.retrieve( - "invocation_id", - ) - - assert_matches_type(AgentAuthInvocationResponse, invocation, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - response = await async_client.agents.auth.invocations.with_raw_response.retrieve( - "invocation_id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - invocation = await response.parse() - assert_matches_type(AgentAuthInvocationResponse, invocation, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - async with async_client.agents.auth.invocations.with_streaming_response.retrieve( - "invocation_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - invocation = await response.parse() - assert_matches_type(AgentAuthInvocationResponse, invocation, 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: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): - await async_client.agents.auth.invocations.with_raw_response.retrieve( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_exchange(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - invocation = await async_client.agents.auth.invocations.exchange( - invocation_id="invocation_id", - code="abc123xyz", - ) - - assert_matches_type(InvocationExchangeResponse, invocation, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_exchange(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - response = await async_client.agents.auth.invocations.with_raw_response.exchange( - invocation_id="invocation_id", - code="abc123xyz", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - invocation = await response.parse() - assert_matches_type(InvocationExchangeResponse, invocation, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_exchange(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - async with async_client.agents.auth.invocations.with_streaming_response.exchange( - invocation_id="invocation_id", - code="abc123xyz", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - invocation = await response.parse() - assert_matches_type(InvocationExchangeResponse, invocation, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_exchange(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): - await async_client.agents.auth.invocations.with_raw_response.exchange( - invocation_id="", - code="abc123xyz", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_submit_overload_1(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - invocation = await async_client.agents.auth.invocations.submit( - invocation_id="invocation_id", - field_values={ - "email": "user@example.com", - "password": "********", - }, - ) - - assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_submit_overload_1(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - response = await async_client.agents.auth.invocations.with_raw_response.submit( - invocation_id="invocation_id", - field_values={ - "email": "user@example.com", - "password": "********", - }, - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - invocation = await response.parse() - assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_submit_overload_1(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - async with async_client.agents.auth.invocations.with_streaming_response.submit( - invocation_id="invocation_id", - field_values={ - "email": "user@example.com", - "password": "********", - }, - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - invocation = await response.parse() - assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_submit_overload_1(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): - await async_client.agents.auth.invocations.with_raw_response.submit( - invocation_id="", - field_values={ - "email": "user@example.com", - "password": "********", - }, - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_submit_overload_2(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - invocation = await async_client.agents.auth.invocations.submit( - invocation_id="invocation_id", - sso_button="xpath=//button[contains(text(), 'Continue with Google')]", - ) - - assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_submit_overload_2(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - response = await async_client.agents.auth.invocations.with_raw_response.submit( - invocation_id="invocation_id", - sso_button="xpath=//button[contains(text(), 'Continue with Google')]", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - invocation = await response.parse() - assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_submit_overload_2(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - async with async_client.agents.auth.invocations.with_streaming_response.submit( - invocation_id="invocation_id", - sso_button="xpath=//button[contains(text(), 'Continue with Google')]", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - invocation = await response.parse() - assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_submit_overload_2(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): - await async_client.agents.auth.invocations.with_raw_response.submit( - invocation_id="", - sso_button="xpath=//button[contains(text(), 'Continue with Google')]", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_submit_overload_3(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - invocation = await async_client.agents.auth.invocations.submit( - invocation_id="invocation_id", - selected_mfa_type="sms", - ) - - assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_submit_overload_3(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - response = await async_client.agents.auth.invocations.with_raw_response.submit( - invocation_id="invocation_id", - selected_mfa_type="sms", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - invocation = await response.parse() - assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_submit_overload_3(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - async with async_client.agents.auth.invocations.with_streaming_response.submit( - invocation_id="invocation_id", - selected_mfa_type="sms", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - invocation = await response.parse() - assert_matches_type(AgentAuthSubmitResponse, invocation, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_submit_overload_3(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - with pytest.raises(ValueError, match=r"Expected a non-empty value for `invocation_id` but received ''"): - await async_client.agents.auth.invocations.with_raw_response.submit( - invocation_id="", - selected_mfa_type="sms", - ) diff --git a/tests/api_resources/agents/test_auth.py b/tests/api_resources/agents/test_auth.py deleted file mode 100644 index c64d77d4..00000000 --- a/tests/api_resources/agents/test_auth.py +++ /dev/null @@ -1,417 +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 kernel import Kernel, AsyncKernel -from tests.utils import assert_matches_type -from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination -from kernel.types.agents import AuthAgent - -# pyright: reportDeprecated=false - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestAuth: - 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: Kernel) -> None: - with pytest.warns(DeprecationWarning): - auth = client.agents.auth.create( - domain="netflix.com", - profile_name="user-123", - ) - - assert_matches_type(AuthAgent, auth, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_create_with_all_params(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - auth = client.agents.auth.create( - domain="netflix.com", - profile_name="user-123", - allowed_domains=["login.netflix.com", "auth.netflix.com"], - credential_name="my-netflix-login", - login_url="https://netflix.com/login", - proxy={"proxy_id": "proxy_id"}, - ) - - assert_matches_type(AuthAgent, auth, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_create(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - response = client.agents.auth.with_raw_response.create( - domain="netflix.com", - profile_name="user-123", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - auth = response.parse() - assert_matches_type(AuthAgent, auth, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_create(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - with client.agents.auth.with_streaming_response.create( - domain="netflix.com", - profile_name="user-123", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - auth = response.parse() - assert_matches_type(AuthAgent, auth, 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: Kernel) -> None: - with pytest.warns(DeprecationWarning): - auth = client.agents.auth.retrieve( - "id", - ) - - assert_matches_type(AuthAgent, auth, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_retrieve(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - response = client.agents.auth.with_raw_response.retrieve( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - auth = response.parse() - assert_matches_type(AuthAgent, auth, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_retrieve(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - with client.agents.auth.with_streaming_response.retrieve( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - auth = response.parse() - assert_matches_type(AuthAgent, auth, 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: Kernel) -> None: - with pytest.warns(DeprecationWarning): - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - client.agents.auth.with_raw_response.retrieve( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_list(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - auth = client.agents.auth.list() - - assert_matches_type(SyncOffsetPagination[AuthAgent], auth, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_list_with_all_params(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - auth = client.agents.auth.list( - domain="domain", - limit=100, - offset=0, - profile_name="profile_name", - ) - - assert_matches_type(SyncOffsetPagination[AuthAgent], auth, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_list(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - response = client.agents.auth.with_raw_response.list() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - auth = response.parse() - assert_matches_type(SyncOffsetPagination[AuthAgent], auth, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_list(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - with client.agents.auth.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - auth = response.parse() - assert_matches_type(SyncOffsetPagination[AuthAgent], auth, 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: Kernel) -> None: - with pytest.warns(DeprecationWarning): - auth = client.agents.auth.delete( - "id", - ) - - assert auth is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_delete(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - response = client.agents.auth.with_raw_response.delete( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - auth = response.parse() - assert auth is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_delete(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - with client.agents.auth.with_streaming_response.delete( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - auth = response.parse() - assert auth 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: Kernel) -> None: - with pytest.warns(DeprecationWarning): - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - client.agents.auth.with_raw_response.delete( - "", - ) - - -class TestAsyncAuth: - 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: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - auth = await async_client.agents.auth.create( - domain="netflix.com", - profile_name="user-123", - ) - - assert_matches_type(AuthAgent, auth, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - auth = await async_client.agents.auth.create( - domain="netflix.com", - profile_name="user-123", - allowed_domains=["login.netflix.com", "auth.netflix.com"], - credential_name="my-netflix-login", - login_url="https://netflix.com/login", - proxy={"proxy_id": "proxy_id"}, - ) - - assert_matches_type(AuthAgent, auth, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_create(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - response = await async_client.agents.auth.with_raw_response.create( - domain="netflix.com", - profile_name="user-123", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - auth = await response.parse() - assert_matches_type(AuthAgent, auth, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - async with async_client.agents.auth.with_streaming_response.create( - domain="netflix.com", - profile_name="user-123", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - auth = await response.parse() - assert_matches_type(AuthAgent, auth, 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: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - auth = await async_client.agents.auth.retrieve( - "id", - ) - - assert_matches_type(AuthAgent, auth, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - response = await async_client.agents.auth.with_raw_response.retrieve( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - auth = await response.parse() - assert_matches_type(AuthAgent, auth, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - async with async_client.agents.auth.with_streaming_response.retrieve( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - auth = await response.parse() - assert_matches_type(AuthAgent, auth, 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: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - await async_client.agents.auth.with_raw_response.retrieve( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_list(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - auth = await async_client.agents.auth.list() - - assert_matches_type(AsyncOffsetPagination[AuthAgent], auth, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - auth = await async_client.agents.auth.list( - domain="domain", - limit=100, - offset=0, - profile_name="profile_name", - ) - - assert_matches_type(AsyncOffsetPagination[AuthAgent], auth, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_list(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - response = await async_client.agents.auth.with_raw_response.list() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - auth = await response.parse() - assert_matches_type(AsyncOffsetPagination[AuthAgent], auth, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - async with async_client.agents.auth.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - auth = await response.parse() - assert_matches_type(AsyncOffsetPagination[AuthAgent], auth, 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: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - auth = await async_client.agents.auth.delete( - "id", - ) - - assert auth is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - response = await async_client.agents.auth.with_raw_response.delete( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - auth = await response.parse() - assert auth is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - async with async_client.agents.auth.with_streaming_response.delete( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - auth = await response.parse() - assert auth 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: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - await async_client.agents.auth.with_raw_response.delete( - "", - ) From 54b8d823cb18f337b5eb90ea889ef6bf0575dee8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 03:55:03 +0000 Subject: [PATCH 291/448] chore(internal): bump dependencies --- requirements-dev.lock | 20 ++++++++++---------- requirements.lock | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/requirements-dev.lock b/requirements-dev.lock index 7643dfba..4a38b203 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -12,14 +12,14 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.13.2 +aiohttp==3.13.3 # via httpx-aiohttp # via kernel aiosignal==1.4.0 # via aiohttp annotated-types==0.7.0 # via pydantic -anyio==4.12.0 +anyio==4.12.1 # via httpx # via kernel argcomplete==3.6.3 @@ -31,7 +31,7 @@ attrs==25.4.0 # via nox backports-asyncio-runner==1.2.0 # via pytest-asyncio -certifi==2025.11.12 +certifi==2026.1.4 # via httpcore # via httpx colorlog==6.10.1 @@ -61,7 +61,7 @@ httpx==0.28.1 # via httpx-aiohttp # via kernel # via respx -httpx-aiohttp==0.1.9 +httpx-aiohttp==0.1.12 # via kernel humanize==4.13.0 # via nox @@ -69,7 +69,7 @@ idna==3.11 # via anyio # via httpx # via yarl -importlib-metadata==8.7.0 +importlib-metadata==8.7.1 iniconfig==2.1.0 # via pytest markdown-it-py==3.0.0 @@ -82,14 +82,14 @@ multidict==6.7.0 mypy==1.17.0 mypy-extensions==1.1.0 # via mypy -nodeenv==1.9.1 +nodeenv==1.10.0 # via pyright nox==2025.11.12 packaging==25.0 # via dependency-groups # via nox # via pytest -pathspec==0.12.1 +pathspec==1.0.3 # via mypy platformdirs==4.4.0 # via virtualenv @@ -115,13 +115,13 @@ python-dateutil==2.9.0.post0 # via time-machine respx==0.22.0 rich==14.2.0 -ruff==0.14.7 +ruff==0.14.13 six==1.17.0 # via python-dateutil sniffio==1.3.1 # via kernel time-machine==2.19.0 -tomli==2.3.0 +tomli==2.4.0 # via dependency-groups # via mypy # via nox @@ -141,7 +141,7 @@ typing-extensions==4.15.0 # via virtualenv typing-inspection==0.4.2 # via pydantic -virtualenv==20.35.4 +virtualenv==20.36.1 # via nox yarl==1.22.0 # via aiohttp diff --git a/requirements.lock b/requirements.lock index bbfe2b35..5f6c7ff4 100644 --- a/requirements.lock +++ b/requirements.lock @@ -12,21 +12,21 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.13.2 +aiohttp==3.13.3 # via httpx-aiohttp # via kernel aiosignal==1.4.0 # via aiohttp annotated-types==0.7.0 # via pydantic -anyio==4.12.0 +anyio==4.12.1 # via httpx # via kernel async-timeout==5.0.1 # via aiohttp attrs==25.4.0 # via aiohttp -certifi==2025.11.12 +certifi==2026.1.4 # via httpcore # via httpx distro==1.9.0 @@ -43,7 +43,7 @@ httpcore==1.0.9 httpx==0.28.1 # via httpx-aiohttp # via kernel -httpx-aiohttp==0.1.9 +httpx-aiohttp==0.1.12 # via kernel idna==3.11 # via anyio From d29150ebbd7edc88465327cab433dd2fb56a68b1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 22:55:57 +0000 Subject: [PATCH 292/448] refactor(auth): simplify proxy configuration in OpenAPI schema --- .stats.yml | 4 +-- src/kernel/resources/auth/connections.py | 26 ++++++++++++++++--- .../types/auth/connection_create_params.py | 17 +++++++++--- .../types/auth/connection_login_params.py | 21 ++++++++++++++- tests/api_resources/auth/test_connections.py | 18 +++++++++++-- 5 files changed, 73 insertions(+), 13 deletions(-) diff --git a/.stats.yml b/.stats.yml index 207d8996..c359ff6b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 100 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-13e82ae9e725e2c3ca19da7248a7a9c8696a0dfe088654cf26aea07c76d6567a.yml -openapi_spec_hash: 6d4151a6066a8474bc56923299aec18a +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-eaf23b9711c16c82563be76641c9c89988288307278dcd630a36f4f186f85afa.yml +openapi_spec_hash: 369570222f4f725e1de11285422837cc config_hash: 82f0a04081a3ab7111d3a9c68cd3ff2b diff --git a/src/kernel/resources/auth/connections.py b/src/kernel/resources/auth/connections.py index 7030b9d1..13aa7825 100644 --- a/src/kernel/resources/auth/connections.py +++ b/src/kernel/resources/auth/connections.py @@ -112,7 +112,8 @@ def create( login_url: Optional login page URL to skip discovery - proxy: Optional proxy configuration + proxy: Proxy selection. Provide either id or name. The proxy must belong to the + caller's org. extra_headers: Send extra headers @@ -315,6 +316,7 @@ def login( self, id: str, *, + proxy: connection_login_params.Proxy | Omit = omit, save_credential_as: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -330,6 +332,9 @@ def login( credentials are stored. Args: + proxy: Proxy selection. Provide either id or name. The proxy must belong to the + caller's org. + save_credential_as: If provided, saves credentials under this name upon successful login extra_headers: Send extra headers @@ -345,7 +350,11 @@ def login( return self._post( f"/auth/connections/{id}/login", body=maybe_transform( - {"save_credential_as": save_credential_as}, connection_login_params.ConnectionLoginParams + { + "proxy": proxy, + "save_credential_as": save_credential_as, + }, + connection_login_params.ConnectionLoginParams, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -485,7 +494,8 @@ async def create( login_url: Optional login page URL to skip discovery - proxy: Optional proxy configuration + proxy: Proxy selection. Provide either id or name. The proxy must belong to the + caller's org. extra_headers: Send extra headers @@ -688,6 +698,7 @@ async def login( self, id: str, *, + proxy: connection_login_params.Proxy | Omit = omit, save_credential_as: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -703,6 +714,9 @@ async def login( credentials are stored. Args: + proxy: Proxy selection. Provide either id or name. The proxy must belong to the + caller's org. + save_credential_as: If provided, saves credentials under this name upon successful login extra_headers: Send extra headers @@ -718,7 +732,11 @@ async def login( return await self._post( f"/auth/connections/{id}/login", body=await async_maybe_transform( - {"save_credential_as": save_credential_as}, connection_login_params.ConnectionLoginParams + { + "proxy": proxy, + "save_credential_as": save_credential_as, + }, + connection_login_params.ConnectionLoginParams, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout diff --git a/src/kernel/types/auth/connection_create_params.py b/src/kernel/types/auth/connection_create_params.py index 2c31f603..91ab5c7e 100644 --- a/src/kernel/types/auth/connection_create_params.py +++ b/src/kernel/types/auth/connection_create_params.py @@ -57,7 +57,10 @@ class ConnectionCreateParams(TypedDict, total=False): """Optional login page URL to skip discovery""" proxy: Proxy - """Optional proxy configuration""" + """Proxy selection. + + Provide either id or name. The proxy must belong to the caller's org. + """ class Credential(TypedDict, total=False): @@ -83,7 +86,13 @@ class Credential(TypedDict, total=False): class Proxy(TypedDict, total=False): - """Optional proxy configuration""" + """Proxy selection. + + Provide either id or name. The proxy must belong to the caller's org. + """ + + id: str + """Proxy ID""" - proxy_id: str - """ID of the proxy to use""" + name: str + """Proxy name""" diff --git a/src/kernel/types/auth/connection_login_params.py b/src/kernel/types/auth/connection_login_params.py index a30d9ff4..bf8326ba 100644 --- a/src/kernel/types/auth/connection_login_params.py +++ b/src/kernel/types/auth/connection_login_params.py @@ -4,9 +4,28 @@ from typing_extensions import TypedDict -__all__ = ["ConnectionLoginParams"] +__all__ = ["ConnectionLoginParams", "Proxy"] class ConnectionLoginParams(TypedDict, total=False): + proxy: Proxy + """Proxy selection. + + Provide either id or name. The proxy must belong to the caller's org. + """ + save_credential_as: str """If provided, saves credentials under this name upon successful login""" + + +class Proxy(TypedDict, total=False): + """Proxy selection. + + Provide either id or name. The proxy must belong to the caller's org. + """ + + id: str + """Proxy ID""" + + name: str + """Proxy name""" diff --git a/tests/api_resources/auth/test_connections.py b/tests/api_resources/auth/test_connections.py index 96ba0a08..9aa66ab8 100644 --- a/tests/api_resources/auth/test_connections.py +++ b/tests/api_resources/auth/test_connections.py @@ -46,7 +46,10 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: }, health_check_interval=3600, login_url="https://netflix.com/login", - proxy={"proxy_id": "proxy_id"}, + proxy={ + "id": "id", + "name": "name", + }, ) assert_matches_type(ManagedAuth, connection, path=["response"]) @@ -255,6 +258,10 @@ def test_method_login(self, client: Kernel) -> None: def test_method_login_with_all_params(self, client: Kernel) -> None: connection = client.auth.connections.login( id="id", + proxy={ + "id": "id", + "name": "name", + }, save_credential_as="my-netflix-login", ) assert_matches_type(LoginResponse, connection, path=["response"]) @@ -395,7 +402,10 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> }, health_check_interval=3600, login_url="https://netflix.com/login", - proxy={"proxy_id": "proxy_id"}, + proxy={ + "id": "id", + "name": "name", + }, ) assert_matches_type(ManagedAuth, connection, path=["response"]) @@ -604,6 +614,10 @@ async def test_method_login(self, async_client: AsyncKernel) -> None: async def test_method_login_with_all_params(self, async_client: AsyncKernel) -> None: connection = await async_client.auth.connections.login( id="id", + proxy={ + "id": "id", + "name": "name", + }, save_credential_as="my-netflix-login", ) assert_matches_type(LoginResponse, connection, path=["response"]) From fcb429f3b2bddfd56e0e319de6bbe9d8ca238fb3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 04:42:42 +0000 Subject: [PATCH 293/448] feat(auth): plan-based min health check intervals --- .stats.yml | 4 ++-- src/kernel/resources/auth/connections.py | 10 ++++++---- src/kernel/types/auth/connection_create_params.py | 5 +++-- src/kernel/types/auth/managed_auth.py | 5 +++-- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/.stats.yml b/.stats.yml index c359ff6b..bc10806d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 100 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-eaf23b9711c16c82563be76641c9c89988288307278dcd630a36f4f186f85afa.yml -openapi_spec_hash: 369570222f4f725e1de11285422837cc +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-92e99f12ad95d7c00047c3f8226cd705174c68c555b42f137f3051768fdf76ae.yml +openapi_spec_hash: 886e96ba621aecde2a3920825771f260 config_hash: 82f0a04081a3ab7111d3a9c68cd3ff2b diff --git a/src/kernel/resources/auth/connections.py b/src/kernel/resources/auth/connections.py index 13aa7825..7c82e7d0 100644 --- a/src/kernel/resources/auth/connections.py +++ b/src/kernel/resources/auth/connections.py @@ -107,8 +107,9 @@ def create( health_check_interval: Interval in seconds between automatic health checks. When set, the system periodically verifies the authentication status and triggers re-authentication - if needed. Must be between 300 (5 minutes) and 86400 (24 hours). Default is 3600 - (1 hour). + if needed. Maximum is 86400 (24 hours). Default is 3600 (1 hour). The minimum + depends on your plan: Enterprise: 300 (5 minutes), Startup: 1200 (20 minutes), + Hobbyist: 3600 (1 hour). login_url: Optional login page URL to skip discovery @@ -489,8 +490,9 @@ async def create( health_check_interval: Interval in seconds between automatic health checks. When set, the system periodically verifies the authentication status and triggers re-authentication - if needed. Must be between 300 (5 minutes) and 86400 (24 hours). Default is 3600 - (1 hour). + if needed. Maximum is 86400 (24 hours). Default is 3600 (1 hour). The minimum + depends on your plan: Enterprise: 300 (5 minutes), Startup: 1200 (20 minutes), + Hobbyist: 3600 (1 hour). login_url: Optional login page URL to skip discovery diff --git a/src/kernel/types/auth/connection_create_params.py b/src/kernel/types/auth/connection_create_params.py index 91ab5c7e..11c24a41 100644 --- a/src/kernel/types/auth/connection_create_params.py +++ b/src/kernel/types/auth/connection_create_params.py @@ -49,8 +49,9 @@ class ConnectionCreateParams(TypedDict, total=False): """Interval in seconds between automatic health checks. When set, the system periodically verifies the authentication status and - triggers re-authentication if needed. Must be between 300 (5 minutes) and 86400 - (24 hours). Default is 3600 (1 hour). + triggers re-authentication if needed. Maximum is 86400 (24 hours). Default is + 3600 (1 hour). The minimum depends on your plan: Enterprise: 300 (5 minutes), + Startup: 1200 (20 minutes), Hobbyist: 3600 (1 hour). """ login_url: str diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py index 9cfc8271..cc40bbe4 100644 --- a/src/kernel/types/auth/managed_auth.py +++ b/src/kernel/types/auth/managed_auth.py @@ -176,8 +176,9 @@ class ManagedAuth(BaseModel): """Interval in seconds between automatic health checks. When set, the system periodically verifies the authentication status and - triggers re-authentication if needed. Must be between 300 (5 minutes) and 86400 - (24 hours). Default is 3600 (1 hour). + triggers re-authentication if needed. Maximum is 86400 (24 hours). Default is + 3600 (1 hour). The minimum depends on your plan: Enterprise: 300 (5 minutes), + Startup: 1200 (20 minutes), Hobbyist: 3600 (1 hour). """ hosted_url: Optional[str] = None From 6f66b7a103a44918cb142191ebc8b8b5d52c6ac8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 10:57:23 +0000 Subject: [PATCH 294/448] feat(auth): add save_credentials support --- .stats.yml | 6 ++-- api.md | 1 - src/kernel/resources/auth/connections.py | 36 ++++++++----------- .../types/auth/connection_create_params.py | 6 ++++ .../types/auth/connection_login_params.py | 3 -- .../types/auth/connection_submit_params.py | 4 +-- src/kernel/types/auth/managed_auth.py | 9 +++++ src/kernel/types/credential.py | 3 ++ tests/api_resources/auth/test_connections.py | 36 ++----------------- 9 files changed, 39 insertions(+), 65 deletions(-) diff --git a/.stats.yml b/.stats.yml index bc10806d..00007d67 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 100 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-92e99f12ad95d7c00047c3f8226cd705174c68c555b42f137f3051768fdf76ae.yml -openapi_spec_hash: 886e96ba621aecde2a3920825771f260 -config_hash: 82f0a04081a3ab7111d3a9c68cd3ff2b +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-82fd51be8dc9b6ad425425f9eb747dd337df494a030d03d37f101e2236c85548.yml +openapi_spec_hash: ab396816e2b7da9938d42ec5a41cc8e1 +config_hash: 27c0ea01aeb797a1767af139851c5b66 diff --git a/api.md b/api.md index 87d0de32..e76371c3 100644 --- a/api.md +++ b/api.md @@ -242,7 +242,6 @@ Types: ```python from kernel.types.auth import ( - LoginRequest, LoginResponse, ManagedAuth, ManagedAuthCreateRequest, diff --git a/src/kernel/resources/auth/connections.py b/src/kernel/resources/auth/connections.py index 7c82e7d0..93b6cf6a 100644 --- a/src/kernel/resources/auth/connections.py +++ b/src/kernel/resources/auth/connections.py @@ -63,6 +63,7 @@ def create( health_check_interval: int | Omit = omit, login_url: str | Omit = omit, proxy: connection_create_params.Proxy | Omit = omit, + save_credentials: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -116,6 +117,9 @@ def create( proxy: Proxy selection. Provide either id or name. The proxy must belong to the caller's org. + save_credentials: Whether to save credentials after every successful login. Defaults to true. + One-time codes (TOTP, SMS, etc.) are not saved. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -135,6 +139,7 @@ def create( "health_check_interval": health_check_interval, "login_url": login_url, "proxy": proxy, + "save_credentials": save_credentials, }, connection_create_params.ConnectionCreateParams, ), @@ -318,7 +323,6 @@ def login( id: str, *, proxy: connection_login_params.Proxy | Omit = omit, - save_credential_as: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -336,8 +340,6 @@ def login( proxy: Proxy selection. Provide either id or name. The proxy must belong to the caller's org. - save_credential_as: If provided, saves credentials under this name upon successful login - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -350,13 +352,7 @@ def login( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( f"/auth/connections/{id}/login", - body=maybe_transform( - { - "proxy": proxy, - "save_credential_as": save_credential_as, - }, - connection_login_params.ConnectionLoginParams, - ), + body=maybe_transform({"proxy": proxy}, connection_login_params.ConnectionLoginParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -367,7 +363,7 @@ def submit( self, id: str, *, - fields: Dict[str, str], + fields: Dict[str, str] | Omit = omit, mfa_option_id: str | Omit = omit, sso_button_selector: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -446,6 +442,7 @@ async def create( health_check_interval: int | Omit = omit, login_url: str | Omit = omit, proxy: connection_create_params.Proxy | Omit = omit, + save_credentials: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -499,6 +496,9 @@ async def create( proxy: Proxy selection. Provide either id or name. The proxy must belong to the caller's org. + save_credentials: Whether to save credentials after every successful login. Defaults to true. + One-time codes (TOTP, SMS, etc.) are not saved. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -518,6 +518,7 @@ async def create( "health_check_interval": health_check_interval, "login_url": login_url, "proxy": proxy, + "save_credentials": save_credentials, }, connection_create_params.ConnectionCreateParams, ), @@ -701,7 +702,6 @@ async def login( id: str, *, proxy: connection_login_params.Proxy | Omit = omit, - save_credential_as: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -719,8 +719,6 @@ async def login( proxy: Proxy selection. Provide either id or name. The proxy must belong to the caller's org. - save_credential_as: If provided, saves credentials under this name upon successful login - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -733,13 +731,7 @@ async def login( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( f"/auth/connections/{id}/login", - body=await async_maybe_transform( - { - "proxy": proxy, - "save_credential_as": save_credential_as, - }, - connection_login_params.ConnectionLoginParams, - ), + body=await async_maybe_transform({"proxy": proxy}, connection_login_params.ConnectionLoginParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -750,7 +742,7 @@ async def submit( self, id: str, *, - fields: Dict[str, str], + fields: Dict[str, str] | Omit = omit, mfa_option_id: str | Omit = omit, sso_button_selector: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. diff --git a/src/kernel/types/auth/connection_create_params.py b/src/kernel/types/auth/connection_create_params.py index 11c24a41..89c787f2 100644 --- a/src/kernel/types/auth/connection_create_params.py +++ b/src/kernel/types/auth/connection_create_params.py @@ -63,6 +63,12 @@ class ConnectionCreateParams(TypedDict, total=False): Provide either id or name. The proxy must belong to the caller's org. """ + save_credentials: bool + """Whether to save credentials after every successful login. + + Defaults to true. One-time codes (TOTP, SMS, etc.) are not saved. + """ + class Credential(TypedDict, total=False): """Reference to credentials for the auth connection. diff --git a/src/kernel/types/auth/connection_login_params.py b/src/kernel/types/auth/connection_login_params.py index bf8326ba..8ea6474c 100644 --- a/src/kernel/types/auth/connection_login_params.py +++ b/src/kernel/types/auth/connection_login_params.py @@ -14,9 +14,6 @@ class ConnectionLoginParams(TypedDict, total=False): Provide either id or name. The proxy must belong to the caller's org. """ - save_credential_as: str - """If provided, saves credentials under this name upon successful login""" - class Proxy(TypedDict, total=False): """Proxy selection. diff --git a/src/kernel/types/auth/connection_submit_params.py b/src/kernel/types/auth/connection_submit_params.py index b299e0a5..0e2306ac 100644 --- a/src/kernel/types/auth/connection_submit_params.py +++ b/src/kernel/types/auth/connection_submit_params.py @@ -3,13 +3,13 @@ from __future__ import annotations from typing import Dict -from typing_extensions import Required, TypedDict +from typing_extensions import TypedDict __all__ = ["ConnectionSubmitParams"] class ConnectionSubmitParams(TypedDict, total=False): - fields: Required[Dict[str, str]] + fields: Dict[str, str] """Map of field name to value""" mfa_option_id: str diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py index cc40bbe4..04a67432 100644 --- a/src/kernel/types/auth/managed_auth.py +++ b/src/kernel/types/auth/managed_auth.py @@ -105,6 +105,12 @@ class ManagedAuth(BaseModel): profile_name: str """Name of the profile associated with this auth connection""" + save_credentials: bool + """Whether credentials are saved after every successful login. + + One-time codes (TOTP, SMS, etc.) are not saved. + """ + status: Literal["AUTHENTICATED", "NEEDS_AUTH"] """Current authentication status of the managed profile""" @@ -202,6 +208,9 @@ class ManagedAuth(BaseModel): post_login_url: Optional[str] = None """URL where the browser landed after successful login""" + proxy_id: Optional[str] = None + """ID of the proxy associated with this connection, if any.""" + sso_provider: Optional[str] = None """SSO provider being used (e.g., google, github, microsoft)""" diff --git a/src/kernel/types/credential.py b/src/kernel/types/credential.py index 8ae733bc..bbf2af5e 100644 --- a/src/kernel/types/credential.py +++ b/src/kernel/types/credential.py @@ -29,6 +29,9 @@ class Credential(BaseModel): has_totp_secret: Optional[bool] = None """Whether this credential has a TOTP secret configured for automatic 2FA""" + has_values: Optional[bool] = None + """Whether this credential has stored values (email, password, etc.)""" + sso_provider: Optional[str] = None """ If set, indicates this credential should be used with the specified SSO provider diff --git a/tests/api_resources/auth/test_connections.py b/tests/api_resources/auth/test_connections.py index 9aa66ab8..f0ab57b6 100644 --- a/tests/api_resources/auth/test_connections.py +++ b/tests/api_resources/auth/test_connections.py @@ -50,6 +50,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: "id": "id", "name": "name", }, + save_credentials=True, ) assert_matches_type(ManagedAuth, connection, path=["response"]) @@ -262,7 +263,6 @@ def test_method_login_with_all_params(self, client: Kernel) -> None: "id": "id", "name": "name", }, - save_credential_as="my-netflix-login", ) assert_matches_type(LoginResponse, connection, path=["response"]) @@ -305,10 +305,6 @@ def test_path_params_login(self, client: Kernel) -> None: def test_method_submit(self, client: Kernel) -> None: connection = client.auth.connections.submit( id="id", - fields={ - "email": "user@example.com", - "password": "secret", - }, ) assert_matches_type(SubmitFieldsResponse, connection, path=["response"]) @@ -331,10 +327,6 @@ def test_method_submit_with_all_params(self, client: Kernel) -> None: def test_raw_response_submit(self, client: Kernel) -> None: response = client.auth.connections.with_raw_response.submit( id="id", - fields={ - "email": "user@example.com", - "password": "secret", - }, ) assert response.is_closed is True @@ -347,10 +339,6 @@ def test_raw_response_submit(self, client: Kernel) -> None: def test_streaming_response_submit(self, client: Kernel) -> None: with client.auth.connections.with_streaming_response.submit( id="id", - fields={ - "email": "user@example.com", - "password": "secret", - }, ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -366,10 +354,6 @@ def test_path_params_submit(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): client.auth.connections.with_raw_response.submit( id="", - fields={ - "email": "user@example.com", - "password": "secret", - }, ) @@ -406,6 +390,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> "id": "id", "name": "name", }, + save_credentials=True, ) assert_matches_type(ManagedAuth, connection, path=["response"]) @@ -618,7 +603,6 @@ async def test_method_login_with_all_params(self, async_client: AsyncKernel) -> "id": "id", "name": "name", }, - save_credential_as="my-netflix-login", ) assert_matches_type(LoginResponse, connection, path=["response"]) @@ -661,10 +645,6 @@ async def test_path_params_login(self, async_client: AsyncKernel) -> None: async def test_method_submit(self, async_client: AsyncKernel) -> None: connection = await async_client.auth.connections.submit( id="id", - fields={ - "email": "user@example.com", - "password": "secret", - }, ) assert_matches_type(SubmitFieldsResponse, connection, path=["response"]) @@ -687,10 +667,6 @@ async def test_method_submit_with_all_params(self, async_client: AsyncKernel) -> async def test_raw_response_submit(self, async_client: AsyncKernel) -> None: response = await async_client.auth.connections.with_raw_response.submit( id="id", - fields={ - "email": "user@example.com", - "password": "secret", - }, ) assert response.is_closed is True @@ -703,10 +679,6 @@ async def test_raw_response_submit(self, async_client: AsyncKernel) -> None: async def test_streaming_response_submit(self, async_client: AsyncKernel) -> None: async with async_client.auth.connections.with_streaming_response.submit( id="id", - fields={ - "email": "user@example.com", - "password": "secret", - }, ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -722,8 +694,4 @@ async def test_path_params_submit(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): await async_client.auth.connections.with_raw_response.submit( id="", - fields={ - "email": "user@example.com", - "password": "secret", - }, ) From cd98e63161c2d51f628172bd0a0eb26c0cd7effc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:07:41 +0000 Subject: [PATCH 295/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index f04d0896..57dc0c3d 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.32.0" + ".": "0.33.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 93afc320..1ede9229 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.32.0" +version = "0.33.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 0247998a..8f653388 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.32.0" # x-release-please-version +__version__ = "0.33.0" # x-release-please-version From 4f23c53843fbf508ad9ef0119f54ebceccc401a0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 03:19:04 +0000 Subject: [PATCH 296/448] chore(internal): fix lint error on Python 3.14 --- src/kernel/_utils/_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kernel/_utils/_compat.py b/src/kernel/_utils/_compat.py index dd703233..2c70b299 100644 --- a/src/kernel/_utils/_compat.py +++ b/src/kernel/_utils/_compat.py @@ -26,7 +26,7 @@ def is_union(tp: Optional[Type[Any]]) -> bool: else: import types - return tp is Union or tp is types.UnionType + return tp is Union or tp is types.UnionType # type: ignore[comparison-overlap] def is_typeddict(tp: Type[Any]) -> bool: From de6f0e1a02af4ed0866888dbabbcaf2a5aba3d7a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 18:00:27 +0000 Subject: [PATCH 297/448] feat: Add error_code field to ManagedAuthSession and related components --- .stats.yml | 4 ++-- src/kernel/types/auth/connection_follow_response.py | 3 +++ src/kernel/types/auth/managed_auth.py | 3 +++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 00007d67..f7218213 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 100 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-82fd51be8dc9b6ad425425f9eb747dd337df494a030d03d37f101e2236c85548.yml -openapi_spec_hash: ab396816e2b7da9938d42ec5a41cc8e1 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-b7d469021adcd1493f74dad38746ffa3817dcf86a0a12561a88eb554824e3ffb.yml +openapi_spec_hash: 4134c95bf3012dca38797ca56d62395b config_hash: 27c0ea01aeb797a1767af139851c5b66 diff --git a/src/kernel/types/auth/connection_follow_response.py b/src/kernel/types/auth/connection_follow_response.py index e54d5c10..06ffaeab 100644 --- a/src/kernel/types/auth/connection_follow_response.py +++ b/src/kernel/types/auth/connection_follow_response.py @@ -95,6 +95,9 @@ class ManagedAuthStateEvent(BaseModel): discovered_fields: Optional[List[ManagedAuthStateEventDiscoveredField]] = None """Fields awaiting input (present when flow_step=AWAITING_INPUT).""" + error_code: Optional[str] = None + """Machine-readable error code (present when flow_status=FAILED).""" + error_message: Optional[str] = None """Error message (present when flow_status=FAILED).""" diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py index 04a67432..de607c9e 100644 --- a/src/kernel/types/auth/managed_auth.py +++ b/src/kernel/types/auth/managed_auth.py @@ -155,6 +155,9 @@ class ManagedAuth(BaseModel): discovered_fields: Optional[List[DiscoveredField]] = None """Fields awaiting input (present when flow_step=awaiting_input)""" + error_code: Optional[str] = None + """Machine-readable error code (present when flow_status=failed)""" + error_message: Optional[str] = None """Error message (present when flow_status=failed)""" From 043d6e90cb1f1a04dc99fc918706702801b6b7b9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 13 Feb 2026 03:12:08 +0000 Subject: [PATCH 298/448] chore: format all `api.md` files --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1ede9229..9eabe4e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,7 +69,7 @@ format = { chain = [ # run formatting again to fix any inconsistencies when imports are stripped "format:ruff", ]} -"format:docs" = "python scripts/utils/ruffen-docs.py README.md api.md" +"format:docs" = "bash -c 'python scripts/utils/ruffen-docs.py README.md $(find . -type f -name api.md)'" "format:ruff" = "ruff format" "lint" = { chain = [ From 07b163e27bf8abfcbfaae975f01c34690750d15d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 13 Feb 2026 22:14:31 +0000 Subject: [PATCH 299/448] feat: Allow arbitrary viewport dimensions --- .stats.yml | 4 +- src/kernel/resources/browser_pools.py | 56 +++++++++---------- src/kernel/resources/browsers/browsers.py | 28 +++++----- src/kernel/types/browser_create_params.py | 14 ++--- src/kernel/types/browser_create_response.py | 14 ++--- src/kernel/types/browser_list_response.py | 14 ++--- src/kernel/types/browser_pool.py | 14 ++--- .../types/browser_pool_acquire_response.py | 14 ++--- .../types/browser_pool_create_params.py | 14 ++--- .../types/browser_pool_update_params.py | 14 ++--- src/kernel/types/browser_retrieve_response.py | 14 ++--- src/kernel/types/browser_update_response.py | 14 ++--- .../invocation_list_browsers_response.py | 14 ++--- src/kernel/types/shared/browser_viewport.py | 9 +-- .../types/shared_params/browser_viewport.py | 9 +-- 15 files changed, 124 insertions(+), 122 deletions(-) diff --git a/.stats.yml b/.stats.yml index f7218213..264b8c9d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 100 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-b7d469021adcd1493f74dad38746ffa3817dcf86a0a12561a88eb554824e3ffb.yml -openapi_spec_hash: 4134c95bf3012dca38797ca56d62395b +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-47ee6a2b624baddb41a681feff758bf1893cd3d65edf3ab51219ebe4d942932b.yml +openapi_spec_hash: 76178c41ede593e76bfacb176057d2f0 config_hash: 27c0ea01aeb797a1767af139851c5b66 diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py index 4f6ad8b2..9177d678 100644 --- a/src/kernel/resources/browser_pools.py +++ b/src/kernel/resources/browser_pools.py @@ -108,13 +108,13 @@ def create( are destroyed. Defaults to 600 seconds if not specified viewport: Initial browser window size in pixels with optional refresh rate. If omitted, - image defaults apply (1920x1080@25). Only specific viewport configurations are - supported. The server will reject unsupported combinations. Supported - resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, - 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will - be automatically determined from the width and height if they match a supported - configuration exactly. Note: Higher resolutions may affect the responsiveness of - live view browser + image defaults apply (1920x1080@25). Arbitrary viewport dimensions are accepted, + but the following configurations are known-good and fully tested: 2560x1440@10, + 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. + Viewports outside this list may exhibit unstable live view or recording + behavior. If refresh_rate is not provided, it will be automatically determined + based on the resolution (higher resolutions use lower refresh rates to keep + bandwidth reasonable). extra_headers: Send extra headers @@ -240,13 +240,13 @@ def update( are destroyed. Defaults to 600 seconds if not specified viewport: Initial browser window size in pixels with optional refresh rate. If omitted, - image defaults apply (1920x1080@25). Only specific viewport configurations are - supported. The server will reject unsupported combinations. Supported - resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, - 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will - be automatically determined from the width and height if they match a supported - configuration exactly. Note: Higher resolutions may affect the responsiveness of - live view browser + image defaults apply (1920x1080@25). Arbitrary viewport dimensions are accepted, + but the following configurations are known-good and fully tested: 2560x1440@10, + 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. + Viewports outside this list may exhibit unstable live view or recording + behavior. If refresh_rate is not provided, it will be automatically determined + based on the resolution (higher resolutions use lower refresh rates to keep + bandwidth reasonable). extra_headers: Send extra headers @@ -546,13 +546,13 @@ async def create( are destroyed. Defaults to 600 seconds if not specified viewport: Initial browser window size in pixels with optional refresh rate. If omitted, - image defaults apply (1920x1080@25). Only specific viewport configurations are - supported. The server will reject unsupported combinations. Supported - resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, - 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will - be automatically determined from the width and height if they match a supported - configuration exactly. Note: Higher resolutions may affect the responsiveness of - live view browser + image defaults apply (1920x1080@25). Arbitrary viewport dimensions are accepted, + but the following configurations are known-good and fully tested: 2560x1440@10, + 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. + Viewports outside this list may exhibit unstable live view or recording + behavior. If refresh_rate is not provided, it will be automatically determined + based on the resolution (higher resolutions use lower refresh rates to keep + bandwidth reasonable). extra_headers: Send extra headers @@ -678,13 +678,13 @@ async def update( are destroyed. Defaults to 600 seconds if not specified viewport: Initial browser window size in pixels with optional refresh rate. If omitted, - image defaults apply (1920x1080@25). Only specific viewport configurations are - supported. The server will reject unsupported combinations. Supported - resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, - 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will - be automatically determined from the width and height if they match a supported - configuration exactly. Note: Higher resolutions may affect the responsiveness of - live view browser + image defaults apply (1920x1080@25). Arbitrary viewport dimensions are accepted, + but the following configurations are known-good and fully tested: 2560x1440@10, + 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. + Viewports outside this list may exhibit unstable live view or recording + behavior. If refresh_rate is not provided, it will be automatically determined + based on the resolution (higher resolutions use lower refresh rates to keep + bandwidth reasonable). extra_headers: Send extra headers diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 4b61d8d8..fe0e5ab9 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -185,13 +185,13 @@ def create( see is +/- 5 seconds around the specified value. viewport: Initial browser window size in pixels with optional refresh rate. If omitted, - image defaults apply (1920x1080@25). Only specific viewport configurations are - supported. The server will reject unsupported combinations. Supported - resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, - 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will - be automatically determined from the width and height if they match a supported - configuration exactly. Note: Higher resolutions may affect the responsiveness of - live view browser + image defaults apply (1920x1080@25). Arbitrary viewport dimensions are accepted, + but the following configurations are known-good and fully tested: 2560x1440@10, + 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. + Viewports outside this list may exhibit unstable live view or recording + behavior. If refresh_rate is not provided, it will be automatically determined + based on the resolution (higher resolutions use lower refresh rates to keep + bandwidth reasonable). extra_headers: Send extra headers @@ -596,13 +596,13 @@ async def create( see is +/- 5 seconds around the specified value. viewport: Initial browser window size in pixels with optional refresh rate. If omitted, - image defaults apply (1920x1080@25). Only specific viewport configurations are - supported. The server will reject unsupported combinations. Supported - resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, - 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will - be automatically determined from the width and height if they match a supported - configuration exactly. Note: Higher resolutions may affect the responsiveness of - live view browser + image defaults apply (1920x1080@25). Arbitrary viewport dimensions are accepted, + but the following configurations are known-good and fully tested: 2560x1440@10, + 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. + Viewports outside this list may exhibit unstable live view or recording + behavior. If refresh_rate is not provided, it will be automatically determined + based on the resolution (higher resolutions use lower refresh rates to keep + bandwidth reasonable). extra_headers: Send extra headers diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index 1e93fa6d..3a6297b9 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -69,11 +69,11 @@ class BrowserCreateParams(TypedDict, total=False): viewport: BrowserViewport """Initial browser window size in pixels with optional refresh rate. - If omitted, image defaults apply (1920x1080@25). Only specific viewport - configurations are supported. The server will reject unsupported combinations. - Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not - provided, it will be automatically determined from the width and height if they - match a supported configuration exactly. Note: Higher resolutions may affect the - responsiveness of live view browser + If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions + are accepted, but the following configurations are known-good and fully tested: + 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, + 1200x800@60. Viewports outside this list may exhibit unstable live view or + recording behavior. If refresh_rate is not provided, it will be automatically + determined based on the resolution (higher resolutions use lower refresh rates + to keep bandwidth reasonable). """ diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index b6c28acf..898f9fa5 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -54,11 +54,11 @@ class BrowserCreateResponse(BaseModel): viewport: Optional[BrowserViewport] = None """Initial browser window size in pixels with optional refresh rate. - If omitted, image defaults apply (1920x1080@25). Only specific viewport - configurations are supported. The server will reject unsupported combinations. - Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not - provided, it will be automatically determined from the width and height if they - match a supported configuration exactly. Note: Higher resolutions may affect the - responsiveness of live view browser + If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions + are accepted, but the following configurations are known-good and fully tested: + 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, + 1200x800@60. Viewports outside this list may exhibit unstable live view or + recording behavior. If refresh_rate is not provided, it will be automatically + determined based on the resolution (higher resolutions use lower refresh rates + to keep bandwidth reasonable). """ diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index d99546d5..f687b176 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -54,11 +54,11 @@ class BrowserListResponse(BaseModel): viewport: Optional[BrowserViewport] = None """Initial browser window size in pixels with optional refresh rate. - If omitted, image defaults apply (1920x1080@25). Only specific viewport - configurations are supported. The server will reject unsupported combinations. - Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not - provided, it will be automatically determined from the width and height if they - match a supported configuration exactly. Note: Higher resolutions may affect the - responsiveness of live view browser + If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions + are accepted, but the following configurations are known-good and fully tested: + 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, + 1200x800@60. Viewports outside this list may exhibit unstable live view or + recording behavior. If refresh_rate is not provided, it will be automatically + determined based on the resolution (higher resolutions use lower refresh rates + to keep bandwidth reasonable). """ diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py index fbbf2cb6..fc4e0f1d 100644 --- a/src/kernel/types/browser_pool.py +++ b/src/kernel/types/browser_pool.py @@ -70,13 +70,13 @@ class BrowserPoolConfig(BaseModel): viewport: Optional[BrowserViewport] = None """Initial browser window size in pixels with optional refresh rate. - If omitted, image defaults apply (1920x1080@25). Only specific viewport - configurations are supported. The server will reject unsupported combinations. - Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not - provided, it will be automatically determined from the width and height if they - match a supported configuration exactly. Note: Higher resolutions may affect the - responsiveness of live view browser + If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions + are accepted, but the following configurations are known-good and fully tested: + 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, + 1200x800@60. Viewports outside this list may exhibit unstable live view or + recording behavior. If refresh_rate is not provided, it will be automatically + determined based on the resolution (higher resolutions use lower refresh rates + to keep bandwidth reasonable). """ diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py index 3175b398..581168c6 100644 --- a/src/kernel/types/browser_pool_acquire_response.py +++ b/src/kernel/types/browser_pool_acquire_response.py @@ -54,11 +54,11 @@ class BrowserPoolAcquireResponse(BaseModel): viewport: Optional[BrowserViewport] = None """Initial browser window size in pixels with optional refresh rate. - If omitted, image defaults apply (1920x1080@25). Only specific viewport - configurations are supported. The server will reject unsupported combinations. - Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not - provided, it will be automatically determined from the width and height if they - match a supported configuration exactly. Note: Higher resolutions may affect the - responsiveness of live view browser + If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions + are accepted, but the following configurations are known-good and fully tested: + 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, + 1200x800@60. Viewports outside this list may exhibit unstable live view or + recording behavior. If refresh_rate is not provided, it will be automatically + determined based on the resolution (higher resolutions use lower refresh rates + to keep bandwidth reasonable). """ diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py index 81deaa68..78268a50 100644 --- a/src/kernel/types/browser_pool_create_params.py +++ b/src/kernel/types/browser_pool_create_params.py @@ -69,11 +69,11 @@ class BrowserPoolCreateParams(TypedDict, total=False): viewport: BrowserViewport """Initial browser window size in pixels with optional refresh rate. - If omitted, image defaults apply (1920x1080@25). Only specific viewport - configurations are supported. The server will reject unsupported combinations. - Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not - provided, it will be automatically determined from the width and height if they - match a supported configuration exactly. Note: Higher resolutions may affect the - responsiveness of live view browser + If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions + are accepted, but the following configurations are known-good and fully tested: + 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, + 1200x800@60. Viewports outside this list may exhibit unstable live view or + recording behavior. If refresh_rate is not provided, it will be automatically + determined based on the resolution (higher resolutions use lower refresh rates + to keep bandwidth reasonable). """ diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py index 63487086..74b76a63 100644 --- a/src/kernel/types/browser_pool_update_params.py +++ b/src/kernel/types/browser_pool_update_params.py @@ -75,11 +75,11 @@ class BrowserPoolUpdateParams(TypedDict, total=False): viewport: BrowserViewport """Initial browser window size in pixels with optional refresh rate. - If omitted, image defaults apply (1920x1080@25). Only specific viewport - configurations are supported. The server will reject unsupported combinations. - Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not - provided, it will be automatically determined from the width and height if they - match a supported configuration exactly. Note: Higher resolutions may affect the - responsiveness of live view browser + If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions + are accepted, but the following configurations are known-good and fully tested: + 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, + 1200x800@60. Viewports outside this list may exhibit unstable live view or + recording behavior. If refresh_rate is not provided, it will be automatically + determined based on the resolution (higher resolutions use lower refresh rates + to keep bandwidth reasonable). """ diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 09210e8c..454cbd64 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -54,11 +54,11 @@ class BrowserRetrieveResponse(BaseModel): viewport: Optional[BrowserViewport] = None """Initial browser window size in pixels with optional refresh rate. - If omitted, image defaults apply (1920x1080@25). Only specific viewport - configurations are supported. The server will reject unsupported combinations. - Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not - provided, it will be automatically determined from the width and height if they - match a supported configuration exactly. Note: Higher resolutions may affect the - responsiveness of live view browser + If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions + are accepted, but the following configurations are known-good and fully tested: + 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, + 1200x800@60. Viewports outside this list may exhibit unstable live view or + recording behavior. If refresh_rate is not provided, it will be automatically + determined based on the resolution (higher resolutions use lower refresh rates + to keep bandwidth reasonable). """ diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py index 01c34be5..8f451736 100644 --- a/src/kernel/types/browser_update_response.py +++ b/src/kernel/types/browser_update_response.py @@ -54,11 +54,11 @@ class BrowserUpdateResponse(BaseModel): viewport: Optional[BrowserViewport] = None """Initial browser window size in pixels with optional refresh rate. - If omitted, image defaults apply (1920x1080@25). Only specific viewport - configurations are supported. The server will reject unsupported combinations. - Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not - provided, it will be automatically determined from the width and height if they - match a supported configuration exactly. Note: Higher resolutions may affect the - responsiveness of live view browser + If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions + are accepted, but the following configurations are known-good and fully tested: + 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, + 1200x800@60. Viewports outside this list may exhibit unstable live view or + recording behavior. If refresh_rate is not provided, it will be automatically + determined based on the resolution (higher resolutions use lower refresh rates + to keep bandwidth reasonable). """ diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py index 4d41a298..619fdcea 100644 --- a/src/kernel/types/invocation_list_browsers_response.py +++ b/src/kernel/types/invocation_list_browsers_response.py @@ -54,13 +54,13 @@ class Browser(BaseModel): viewport: Optional[BrowserViewport] = None """Initial browser window size in pixels with optional refresh rate. - If omitted, image defaults apply (1920x1080@25). Only specific viewport - configurations are supported. The server will reject unsupported combinations. - Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not - provided, it will be automatically determined from the width and height if they - match a supported configuration exactly. Note: Higher resolutions may affect the - responsiveness of live view browser + If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions + are accepted, but the following configurations are known-good and fully tested: + 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, + 1200x800@60. Viewports outside this list may exhibit unstable live view or + recording behavior. If refresh_rate is not provided, it will be automatically + determined based on the resolution (higher resolutions use lower refresh rates + to keep bandwidth reasonable). """ diff --git a/src/kernel/types/shared/browser_viewport.py b/src/kernel/types/shared/browser_viewport.py index 2329dd75..dacac1f2 100644 --- a/src/kernel/types/shared/browser_viewport.py +++ b/src/kernel/types/shared/browser_viewport.py @@ -11,10 +11,11 @@ class BrowserViewport(BaseModel): """Initial browser window size in pixels with optional refresh rate. If omitted, image defaults apply (1920x1080@25). - Only specific viewport configurations are supported. The server will reject unsupported combinations. - Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 - If refresh_rate is not provided, it will be automatically determined from the width and height if they match a supported configuration exactly. - Note: Higher resolutions may affect the responsiveness of live view browser + Arbitrary viewport dimensions are accepted, but the following configurations are known-good and fully tested: + 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. + Viewports outside this list may exhibit unstable live view or recording behavior. + If refresh_rate is not provided, it will be automatically determined based on the resolution + (higher resolutions use lower refresh rates to keep bandwidth reasonable). """ height: int diff --git a/src/kernel/types/shared_params/browser_viewport.py b/src/kernel/types/shared_params/browser_viewport.py index 7041ea55..f98ece82 100644 --- a/src/kernel/types/shared_params/browser_viewport.py +++ b/src/kernel/types/shared_params/browser_viewport.py @@ -11,10 +11,11 @@ class BrowserViewport(TypedDict, total=False): """Initial browser window size in pixels with optional refresh rate. If omitted, image defaults apply (1920x1080@25). - Only specific viewport configurations are supported. The server will reject unsupported combinations. - Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 - If refresh_rate is not provided, it will be automatically determined from the width and height if they match a supported configuration exactly. - Note: Higher resolutions may affect the responsiveness of live view browser + Arbitrary viewport dimensions are accepted, but the following configurations are known-good and fully tested: + 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. + Viewports outside this list may exhibit unstable live view or recording behavior. + If refresh_rate is not provided, it will be automatically determined based on the resolution + (higher resolutions use lower refresh rates to keep bandwidth reasonable). """ height: Required[int] From d36301469e80fbb6b1a02f35eb7de7e4077cfb66 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:47:22 +0000 Subject: [PATCH 300/448] feat: Neil/kernel 873 templates v4 --- .stats.yml | 4 ++-- src/kernel/types/shared/app_action.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 264b8c9d..1c121c21 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 100 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-47ee6a2b624baddb41a681feff758bf1893cd3d65edf3ab51219ebe4d942932b.yml -openapi_spec_hash: 76178c41ede593e76bfacb176057d2f0 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2b77b2efd4d25aaa264cbd6fcb0e43f82d14ce5a4bd6fb1e3859be440868685a.yml +openapi_spec_hash: 299be31ecb4a96dcd54d4d902a716e68 config_hash: 27c0ea01aeb797a1767af139851c5b66 diff --git a/src/kernel/types/shared/app_action.py b/src/kernel/types/shared/app_action.py index 1babce1d..753bed78 100644 --- a/src/kernel/types/shared/app_action.py +++ b/src/kernel/types/shared/app_action.py @@ -1,5 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from typing import Dict, Optional + from ..._models import BaseModel __all__ = ["AppAction"] @@ -10,3 +12,15 @@ class AppAction(BaseModel): name: str """Name of the action""" + + input_schema: Optional[Dict[str, object]] = None + """JSON Schema (draft-07) describing the expected input payload. + + Null if schema could not be automatically generated. + """ + + output_schema: Optional[Dict[str, object]] = None + """JSON Schema (draft-07) describing the expected output payload. + + Null if schema could not be automatically generated. + """ From a27a7e38b651a1d78d3cd9db3336920cf72f8c48 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:55:23 +0000 Subject: [PATCH 301/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 57dc0c3d..e4e1c3ce 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.33.0" + ".": "0.34.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 9eabe4e5..b7f4fe64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.33.0" +version = "0.34.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 8f653388..23d2f207 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.33.0" # x-release-please-version +__version__ = "0.34.0" # x-release-please-version From 3a317e897606e97a3341b71d186369c030674541 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 20:16:46 +0000 Subject: [PATCH 302/448] feat: GPU pools --- .stats.yml | 4 ++-- src/kernel/resources/browsers/browsers.py | 10 ++++++++++ src/kernel/types/browser_create_params.py | 6 ++++++ src/kernel/types/browser_create_response.py | 3 +++ src/kernel/types/browser_list_response.py | 3 +++ src/kernel/types/browser_pool_acquire_response.py | 3 +++ src/kernel/types/browser_retrieve_response.py | 3 +++ src/kernel/types/browser_update_response.py | 3 +++ src/kernel/types/invocation_list_browsers_response.py | 3 +++ tests/api_resources/test_browsers.py | 2 ++ 10 files changed, 38 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 1c121c21..92d7f1dc 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 100 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2b77b2efd4d25aaa264cbd6fcb0e43f82d14ce5a4bd6fb1e3859be440868685a.yml -openapi_spec_hash: 299be31ecb4a96dcd54d4d902a716e68 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-a0f1d08e6f62a74de2aac5c25e592494abdd59f2cfca2842c5810927554faee0.yml +openapi_spec_hash: ebd8bf67b7bb371cf4b4fa68b967cab5 config_hash: 27c0ea01aeb797a1767af139851c5b66 diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index fe0e5ab9..58c3e2a3 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -136,6 +136,7 @@ def create( self, *, extensions: Iterable[BrowserExtension] | Omit = omit, + gpu: bool | Omit = omit, headless: bool | Omit = omit, invocation_id: str | Omit = omit, kiosk_mode: bool | Omit = omit, @@ -158,6 +159,9 @@ def create( Args: extensions: List of browser extensions to load into the session. Provide each by id or name. + gpu: If true, launches a hardware-accelerated browser with GPU rendering. Requires + Start-Up or Enterprise plan. + headless: If true, launches the browser using a headless image (no VNC/GUI). Defaults to false. @@ -206,6 +210,7 @@ def create( body=maybe_transform( { "extensions": extensions, + "gpu": gpu, "headless": headless, "invocation_id": invocation_id, "kiosk_mode": kiosk_mode, @@ -547,6 +552,7 @@ async def create( self, *, extensions: Iterable[BrowserExtension] | Omit = omit, + gpu: bool | Omit = omit, headless: bool | Omit = omit, invocation_id: str | Omit = omit, kiosk_mode: bool | Omit = omit, @@ -569,6 +575,9 @@ async def create( Args: extensions: List of browser extensions to load into the session. Provide each by id or name. + gpu: If true, launches a hardware-accelerated browser with GPU rendering. Requires + Start-Up or Enterprise plan. + headless: If true, launches the browser using a headless image (no VNC/GUI). Defaults to false. @@ -617,6 +626,7 @@ async def create( body=await async_maybe_transform( { "extensions": extensions, + "gpu": gpu, "headless": headless, "invocation_id": invocation_id, "kiosk_mode": kiosk_mode, diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index 3a6297b9..6df24637 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -20,6 +20,12 @@ class BrowserCreateParams(TypedDict, total=False): Provide each by id or name. """ + gpu: bool + """If true, launches a hardware-accelerated browser with GPU rendering. + + Requires Start-Up or Enterprise plan. + """ + headless: bool """If true, launches the browser using a headless image (no VNC/GUI). diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index 898f9fa5..051d739f 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -39,6 +39,9 @@ class BrowserCreateResponse(BaseModel): deleted_at: Optional[datetime] = None """When the browser session was soft-deleted. Only present for deleted sessions.""" + gpu: Optional[bool] = None + """Whether the browser session has hardware-accelerated GPU rendering.""" + kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index f687b176..85d1dd1a 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -39,6 +39,9 @@ class BrowserListResponse(BaseModel): deleted_at: Optional[datetime] = None """When the browser session was soft-deleted. Only present for deleted sessions.""" + gpu: Optional[bool] = None + """Whether the browser session has hardware-accelerated GPU rendering.""" + kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py index 581168c6..b8d066d8 100644 --- a/src/kernel/types/browser_pool_acquire_response.py +++ b/src/kernel/types/browser_pool_acquire_response.py @@ -39,6 +39,9 @@ class BrowserPoolAcquireResponse(BaseModel): deleted_at: Optional[datetime] = None """When the browser session was soft-deleted. Only present for deleted sessions.""" + gpu: Optional[bool] = None + """Whether the browser session has hardware-accelerated GPU rendering.""" + kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 454cbd64..ee99dcd5 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -39,6 +39,9 @@ class BrowserRetrieveResponse(BaseModel): deleted_at: Optional[datetime] = None """When the browser session was soft-deleted. Only present for deleted sessions.""" + gpu: Optional[bool] = None + """Whether the browser session has hardware-accelerated GPU rendering.""" + kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py index 8f451736..6591144e 100644 --- a/src/kernel/types/browser_update_response.py +++ b/src/kernel/types/browser_update_response.py @@ -39,6 +39,9 @@ class BrowserUpdateResponse(BaseModel): deleted_at: Optional[datetime] = None """When the browser session was soft-deleted. Only present for deleted sessions.""" + gpu: Optional[bool] = None + """Whether the browser session has hardware-accelerated GPU rendering.""" + kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py index 619fdcea..a1b1a08b 100644 --- a/src/kernel/types/invocation_list_browsers_response.py +++ b/src/kernel/types/invocation_list_browsers_response.py @@ -39,6 +39,9 @@ class Browser(BaseModel): deleted_at: Optional[datetime] = None """When the browser session was soft-deleted. Only present for deleted sessions.""" + gpu: Optional[bool] = None + """Whether the browser session has hardware-accelerated GPU rendering.""" + kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index 914b5af6..fc2d4d09 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -41,6 +41,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: "name": "name", } ], + gpu=False, headless=False, invocation_id="rr33xuugxj9h0bkf1rdt2bet", kiosk_mode=True, @@ -402,6 +403,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> "name": "name", } ], + gpu=False, headless=False, invocation_id="rr33xuugxj9h0bkf1rdt2bet", kiosk_mode=True, From 8b460ed940087a4b718538a08395f77f95826d73 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 19:20:48 +0000 Subject: [PATCH 303/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e4e1c3ce..ce5e5c7c 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.34.0" + ".": "0.35.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index b7f4fe64..75cbfbee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.34.0" +version = "0.35.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 23d2f207..ccc3c759 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.34.0" # x-release-please-version +__version__ = "0.35.0" # x-release-please-version From 8f0271313350a1d555e679a10f032a81a809883b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 03:24:31 +0000 Subject: [PATCH 304/448] chore(internal): remove mock server code --- scripts/mock | 41 ----------------------------------------- scripts/test | 46 ---------------------------------------------- 2 files changed, 87 deletions(-) delete mode 100755 scripts/mock diff --git a/scripts/mock b/scripts/mock deleted file mode 100755 index 0b28f6ea..00000000 --- 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 index dbeda2d2..39729d09 100755 --- a/scripts/test +++ b/scripts/test @@ -4,53 +4,7 @@ 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 From 04b99a283d42e1479af4b04a81afd1b2c8d0a389 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 03:25:46 +0000 Subject: [PATCH 305/448] chore: update mock server docs --- CONTRIBUTING.md | 7 - tests/api_resources/auth/test_connections.py | 120 ++++++------ tests/api_resources/browsers/fs/test_watch.py | 52 +++--- tests/api_resources/browsers/test_computer.py | 168 ++++++++--------- tests/api_resources/browsers/test_fs.py | 172 +++++++++--------- tests/api_resources/browsers/test_logs.py | 20 +- .../api_resources/browsers/test_playwright.py | 20 +- tests/api_resources/browsers/test_process.py | 120 ++++++------ tests/api_resources/browsers/test_replays.py | 52 +++--- tests/api_resources/test_apps.py | 16 +- tests/api_resources/test_browser_pools.py | 140 +++++++------- tests/api_resources/test_browsers.py | 116 ++++++------ .../test_credential_providers.py | 112 ++++++------ tests/api_resources/test_credentials.py | 100 +++++----- tests/api_resources/test_deployments.py | 68 +++---- tests/api_resources/test_extensions.py | 44 ++--- tests/api_resources/test_invocations.py | 120 ++++++------ tests/api_resources/test_profiles.py | 60 +++--- tests/api_resources/test_proxies.py | 76 ++++---- 19 files changed, 788 insertions(+), 795 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9cb624fb..b5bef2f2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -85,13 +85,6 @@ $ 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 ``` diff --git a/tests/api_resources/auth/test_connections.py b/tests/api_resources/auth/test_connections.py index f0ab57b6..2e208663 100644 --- a/tests/api_resources/auth/test_connections.py +++ b/tests/api_resources/auth/test_connections.py @@ -22,7 +22,7 @@ class TestConnections: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create(self, client: Kernel) -> None: connection = client.auth.connections.create( @@ -31,7 +31,7 @@ def test_method_create(self, client: Kernel) -> None: ) assert_matches_type(ManagedAuth, connection, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create_with_all_params(self, client: Kernel) -> None: connection = client.auth.connections.create( @@ -54,7 +54,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(ManagedAuth, connection, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_create(self, client: Kernel) -> None: response = client.auth.connections.with_raw_response.create( @@ -67,7 +67,7 @@ def test_raw_response_create(self, client: Kernel) -> None: connection = response.parse() assert_matches_type(ManagedAuth, connection, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_create(self, client: Kernel) -> None: with client.auth.connections.with_streaming_response.create( @@ -82,7 +82,7 @@ def test_streaming_response_create(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_retrieve(self, client: Kernel) -> None: connection = client.auth.connections.retrieve( @@ -90,7 +90,7 @@ def test_method_retrieve(self, client: Kernel) -> None: ) assert_matches_type(ManagedAuth, connection, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_retrieve(self, client: Kernel) -> None: response = client.auth.connections.with_raw_response.retrieve( @@ -102,7 +102,7 @@ def test_raw_response_retrieve(self, client: Kernel) -> None: connection = response.parse() assert_matches_type(ManagedAuth, connection, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_retrieve(self, client: Kernel) -> None: with client.auth.connections.with_streaming_response.retrieve( @@ -116,7 +116,7 @@ def test_streaming_response_retrieve(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_retrieve(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -124,13 +124,13 @@ def test_path_params_retrieve(self, client: Kernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_list(self, client: Kernel) -> None: connection = client.auth.connections.list() assert_matches_type(SyncOffsetPagination[ManagedAuth], connection, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_list_with_all_params(self, client: Kernel) -> None: connection = client.auth.connections.list( @@ -141,7 +141,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(SyncOffsetPagination[ManagedAuth], connection, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_list(self, client: Kernel) -> None: response = client.auth.connections.with_raw_response.list() @@ -151,7 +151,7 @@ def test_raw_response_list(self, client: Kernel) -> None: connection = response.parse() assert_matches_type(SyncOffsetPagination[ManagedAuth], connection, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_list(self, client: Kernel) -> None: with client.auth.connections.with_streaming_response.list() as response: @@ -163,7 +163,7 @@ def test_streaming_response_list(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_delete(self, client: Kernel) -> None: connection = client.auth.connections.delete( @@ -171,7 +171,7 @@ def test_method_delete(self, client: Kernel) -> None: ) assert connection is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_delete(self, client: Kernel) -> None: response = client.auth.connections.with_raw_response.delete( @@ -183,7 +183,7 @@ def test_raw_response_delete(self, client: Kernel) -> None: connection = response.parse() assert connection is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_delete(self, client: Kernel) -> None: with client.auth.connections.with_streaming_response.delete( @@ -197,7 +197,7 @@ def test_streaming_response_delete(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_delete(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -205,7 +205,7 @@ def test_path_params_delete(self, client: Kernel) -> None: "", ) - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize def test_method_follow(self, client: Kernel) -> None: connection_stream = client.auth.connections.follow( @@ -213,7 +213,7 @@ def test_method_follow(self, client: Kernel) -> None: ) connection_stream.response.close() - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize def test_raw_response_follow(self, client: Kernel) -> None: response = client.auth.connections.with_raw_response.follow( @@ -224,7 +224,7 @@ def test_raw_response_follow(self, client: Kernel) -> None: stream = response.parse() stream.close() - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize def test_streaming_response_follow(self, client: Kernel) -> None: with client.auth.connections.with_streaming_response.follow( @@ -238,7 +238,7 @@ def test_streaming_response_follow(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize def test_path_params_follow(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -246,7 +246,7 @@ def test_path_params_follow(self, client: Kernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_login(self, client: Kernel) -> None: connection = client.auth.connections.login( @@ -254,7 +254,7 @@ def test_method_login(self, client: Kernel) -> None: ) assert_matches_type(LoginResponse, connection, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_login_with_all_params(self, client: Kernel) -> None: connection = client.auth.connections.login( @@ -266,7 +266,7 @@ def test_method_login_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(LoginResponse, connection, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_login(self, client: Kernel) -> None: response = client.auth.connections.with_raw_response.login( @@ -278,7 +278,7 @@ def test_raw_response_login(self, client: Kernel) -> None: connection = response.parse() assert_matches_type(LoginResponse, connection, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_login(self, client: Kernel) -> None: with client.auth.connections.with_streaming_response.login( @@ -292,7 +292,7 @@ def test_streaming_response_login(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_login(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -300,7 +300,7 @@ def test_path_params_login(self, client: Kernel) -> None: id="", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_submit(self, client: Kernel) -> None: connection = client.auth.connections.submit( @@ -308,7 +308,7 @@ def test_method_submit(self, client: Kernel) -> None: ) assert_matches_type(SubmitFieldsResponse, connection, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_submit_with_all_params(self, client: Kernel) -> None: connection = client.auth.connections.submit( @@ -322,7 +322,7 @@ def test_method_submit_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(SubmitFieldsResponse, connection, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_submit(self, client: Kernel) -> None: response = client.auth.connections.with_raw_response.submit( @@ -334,7 +334,7 @@ def test_raw_response_submit(self, client: Kernel) -> None: connection = response.parse() assert_matches_type(SubmitFieldsResponse, connection, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_submit(self, client: Kernel) -> None: with client.auth.connections.with_streaming_response.submit( @@ -348,7 +348,7 @@ def test_streaming_response_submit(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_submit(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -362,7 +362,7 @@ class TestAsyncConnections: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create(self, async_client: AsyncKernel) -> None: connection = await async_client.auth.connections.create( @@ -371,7 +371,7 @@ async def test_method_create(self, async_client: AsyncKernel) -> None: ) assert_matches_type(ManagedAuth, connection, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: connection = await async_client.auth.connections.create( @@ -394,7 +394,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> ) assert_matches_type(ManagedAuth, connection, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_create(self, async_client: AsyncKernel) -> None: response = await async_client.auth.connections.with_raw_response.create( @@ -407,7 +407,7 @@ async def test_raw_response_create(self, async_client: AsyncKernel) -> None: connection = await response.parse() assert_matches_type(ManagedAuth, connection, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: async with async_client.auth.connections.with_streaming_response.create( @@ -422,7 +422,7 @@ async def test_streaming_response_create(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_retrieve(self, async_client: AsyncKernel) -> None: connection = await async_client.auth.connections.retrieve( @@ -430,7 +430,7 @@ async def test_method_retrieve(self, async_client: AsyncKernel) -> None: ) assert_matches_type(ManagedAuth, connection, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: response = await async_client.auth.connections.with_raw_response.retrieve( @@ -442,7 +442,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: connection = await response.parse() assert_matches_type(ManagedAuth, connection, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: async with async_client.auth.connections.with_streaming_response.retrieve( @@ -456,7 +456,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> N assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -464,13 +464,13 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_list(self, async_client: AsyncKernel) -> None: connection = await async_client.auth.connections.list() assert_matches_type(AsyncOffsetPagination[ManagedAuth], connection, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: connection = await async_client.auth.connections.list( @@ -481,7 +481,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N ) assert_matches_type(AsyncOffsetPagination[ManagedAuth], connection, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_list(self, async_client: AsyncKernel) -> None: response = await async_client.auth.connections.with_raw_response.list() @@ -491,7 +491,7 @@ async def test_raw_response_list(self, async_client: AsyncKernel) -> None: connection = await response.parse() assert_matches_type(AsyncOffsetPagination[ManagedAuth], connection, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: async with async_client.auth.connections.with_streaming_response.list() as response: @@ -503,7 +503,7 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_delete(self, async_client: AsyncKernel) -> None: connection = await async_client.auth.connections.delete( @@ -511,7 +511,7 @@ async def test_method_delete(self, async_client: AsyncKernel) -> None: ) assert connection is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: response = await async_client.auth.connections.with_raw_response.delete( @@ -523,7 +523,7 @@ async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: connection = await response.parse() assert connection is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None: async with async_client.auth.connections.with_streaming_response.delete( @@ -537,7 +537,7 @@ async def test_streaming_response_delete(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_delete(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -545,7 +545,7 @@ async def test_path_params_delete(self, async_client: AsyncKernel) -> None: "", ) - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize async def test_method_follow(self, async_client: AsyncKernel) -> None: connection_stream = await async_client.auth.connections.follow( @@ -553,7 +553,7 @@ async def test_method_follow(self, async_client: AsyncKernel) -> None: ) await connection_stream.response.aclose() - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize async def test_raw_response_follow(self, async_client: AsyncKernel) -> None: response = await async_client.auth.connections.with_raw_response.follow( @@ -564,7 +564,7 @@ async def test_raw_response_follow(self, async_client: AsyncKernel) -> None: stream = await response.parse() await stream.close() - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize async def test_streaming_response_follow(self, async_client: AsyncKernel) -> None: async with async_client.auth.connections.with_streaming_response.follow( @@ -578,7 +578,7 @@ async def test_streaming_response_follow(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize async def test_path_params_follow(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -586,7 +586,7 @@ async def test_path_params_follow(self, async_client: AsyncKernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_login(self, async_client: AsyncKernel) -> None: connection = await async_client.auth.connections.login( @@ -594,7 +594,7 @@ async def test_method_login(self, async_client: AsyncKernel) -> None: ) assert_matches_type(LoginResponse, connection, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_login_with_all_params(self, async_client: AsyncKernel) -> None: connection = await async_client.auth.connections.login( @@ -606,7 +606,7 @@ async def test_method_login_with_all_params(self, async_client: AsyncKernel) -> ) assert_matches_type(LoginResponse, connection, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_login(self, async_client: AsyncKernel) -> None: response = await async_client.auth.connections.with_raw_response.login( @@ -618,7 +618,7 @@ async def test_raw_response_login(self, async_client: AsyncKernel) -> None: connection = await response.parse() assert_matches_type(LoginResponse, connection, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_login(self, async_client: AsyncKernel) -> None: async with async_client.auth.connections.with_streaming_response.login( @@ -632,7 +632,7 @@ async def test_streaming_response_login(self, async_client: AsyncKernel) -> None assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_login(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -640,7 +640,7 @@ async def test_path_params_login(self, async_client: AsyncKernel) -> None: id="", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_submit(self, async_client: AsyncKernel) -> None: connection = await async_client.auth.connections.submit( @@ -648,7 +648,7 @@ async def test_method_submit(self, async_client: AsyncKernel) -> None: ) assert_matches_type(SubmitFieldsResponse, connection, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_submit_with_all_params(self, async_client: AsyncKernel) -> None: connection = await async_client.auth.connections.submit( @@ -662,7 +662,7 @@ async def test_method_submit_with_all_params(self, async_client: AsyncKernel) -> ) assert_matches_type(SubmitFieldsResponse, connection, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_submit(self, async_client: AsyncKernel) -> None: response = await async_client.auth.connections.with_raw_response.submit( @@ -674,7 +674,7 @@ async def test_raw_response_submit(self, async_client: AsyncKernel) -> None: connection = await response.parse() assert_matches_type(SubmitFieldsResponse, connection, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_submit(self, async_client: AsyncKernel) -> None: async with async_client.auth.connections.with_streaming_response.submit( @@ -688,7 +688,7 @@ async def test_streaming_response_submit(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_submit(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): diff --git a/tests/api_resources/browsers/fs/test_watch.py b/tests/api_resources/browsers/fs/test_watch.py index 683e1549..48b3dcf5 100644 --- a/tests/api_resources/browsers/fs/test_watch.py +++ b/tests/api_resources/browsers/fs/test_watch.py @@ -17,7 +17,7 @@ class TestWatch: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize def test_method_events(self, client: Kernel) -> None: watch_stream = client.browsers.fs.watch.events( @@ -26,7 +26,7 @@ def test_method_events(self, client: Kernel) -> None: ) watch_stream.response.close() - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize def test_raw_response_events(self, client: Kernel) -> None: response = client.browsers.fs.watch.with_raw_response.events( @@ -38,7 +38,7 @@ def test_raw_response_events(self, client: Kernel) -> None: stream = response.parse() stream.close() - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize def test_streaming_response_events(self, client: Kernel) -> None: with client.browsers.fs.watch.with_streaming_response.events( @@ -53,7 +53,7 @@ def test_streaming_response_events(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize def test_path_params_events(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -68,7 +68,7 @@ def test_path_params_events(self, client: Kernel) -> None: id="id", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_start(self, client: Kernel) -> None: watch = client.browsers.fs.watch.start( @@ -77,7 +77,7 @@ def test_method_start(self, client: Kernel) -> None: ) assert_matches_type(WatchStartResponse, watch, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_start_with_all_params(self, client: Kernel) -> None: watch = client.browsers.fs.watch.start( @@ -87,7 +87,7 @@ def test_method_start_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(WatchStartResponse, watch, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_start(self, client: Kernel) -> None: response = client.browsers.fs.watch.with_raw_response.start( @@ -100,7 +100,7 @@ def test_raw_response_start(self, client: Kernel) -> None: watch = response.parse() assert_matches_type(WatchStartResponse, watch, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_start(self, client: Kernel) -> None: with client.browsers.fs.watch.with_streaming_response.start( @@ -115,7 +115,7 @@ def test_streaming_response_start(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_start(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -124,7 +124,7 @@ def test_path_params_start(self, client: Kernel) -> None: path="path", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_stop(self, client: Kernel) -> None: watch = client.browsers.fs.watch.stop( @@ -133,7 +133,7 @@ def test_method_stop(self, client: Kernel) -> None: ) assert watch is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_stop(self, client: Kernel) -> None: response = client.browsers.fs.watch.with_raw_response.stop( @@ -146,7 +146,7 @@ def test_raw_response_stop(self, client: Kernel) -> None: watch = response.parse() assert watch is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_stop(self, client: Kernel) -> None: with client.browsers.fs.watch.with_streaming_response.stop( @@ -161,7 +161,7 @@ def test_streaming_response_stop(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_stop(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -182,7 +182,7 @@ class TestAsyncWatch: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize async def test_method_events(self, async_client: AsyncKernel) -> None: watch_stream = await async_client.browsers.fs.watch.events( @@ -191,7 +191,7 @@ async def test_method_events(self, async_client: AsyncKernel) -> None: ) await watch_stream.response.aclose() - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize async def test_raw_response_events(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.fs.watch.with_raw_response.events( @@ -203,7 +203,7 @@ async def test_raw_response_events(self, async_client: AsyncKernel) -> None: stream = await response.parse() await stream.close() - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize async def test_streaming_response_events(self, async_client: AsyncKernel) -> None: async with async_client.browsers.fs.watch.with_streaming_response.events( @@ -218,7 +218,7 @@ async def test_streaming_response_events(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize async def test_path_params_events(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -233,7 +233,7 @@ async def test_path_params_events(self, async_client: AsyncKernel) -> None: id="id", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_start(self, async_client: AsyncKernel) -> None: watch = await async_client.browsers.fs.watch.start( @@ -242,7 +242,7 @@ async def test_method_start(self, async_client: AsyncKernel) -> None: ) assert_matches_type(WatchStartResponse, watch, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_start_with_all_params(self, async_client: AsyncKernel) -> None: watch = await async_client.browsers.fs.watch.start( @@ -252,7 +252,7 @@ async def test_method_start_with_all_params(self, async_client: AsyncKernel) -> ) assert_matches_type(WatchStartResponse, watch, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_start(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.fs.watch.with_raw_response.start( @@ -265,7 +265,7 @@ async def test_raw_response_start(self, async_client: AsyncKernel) -> None: watch = await response.parse() assert_matches_type(WatchStartResponse, watch, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_start(self, async_client: AsyncKernel) -> None: async with async_client.browsers.fs.watch.with_streaming_response.start( @@ -280,7 +280,7 @@ async def test_streaming_response_start(self, async_client: AsyncKernel) -> None assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_start(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -289,7 +289,7 @@ async def test_path_params_start(self, async_client: AsyncKernel) -> None: path="path", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_stop(self, async_client: AsyncKernel) -> None: watch = await async_client.browsers.fs.watch.stop( @@ -298,7 +298,7 @@ async def test_method_stop(self, async_client: AsyncKernel) -> None: ) assert watch is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_stop(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.fs.watch.with_raw_response.stop( @@ -311,7 +311,7 @@ async def test_raw_response_stop(self, async_client: AsyncKernel) -> None: watch = await response.parse() assert watch is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_stop(self, async_client: AsyncKernel) -> None: async with async_client.browsers.fs.watch.with_streaming_response.stop( @@ -326,7 +326,7 @@ async def test_streaming_response_stop(self, async_client: AsyncKernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_stop(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): diff --git a/tests/api_resources/browsers/test_computer.py b/tests/api_resources/browsers/test_computer.py index f98f81af..091fc91d 100644 --- a/tests/api_resources/browsers/test_computer.py +++ b/tests/api_resources/browsers/test_computer.py @@ -28,7 +28,7 @@ class TestComputer: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_batch(self, client: Kernel) -> None: computer = client.browsers.computer.batch( @@ -37,7 +37,7 @@ def test_method_batch(self, client: Kernel) -> None: ) assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_batch(self, client: Kernel) -> None: response = client.browsers.computer.with_raw_response.batch( @@ -50,7 +50,7 @@ def test_raw_response_batch(self, client: Kernel) -> None: computer = response.parse() assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_batch(self, client: Kernel) -> None: with client.browsers.computer.with_streaming_response.batch( @@ -65,7 +65,7 @@ def test_streaming_response_batch(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_batch(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -142,7 +142,7 @@ def test_path_params_capture_screenshot(self, client: Kernel) -> None: id="", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_click_mouse(self, client: Kernel) -> None: computer = client.browsers.computer.click_mouse( @@ -152,7 +152,7 @@ def test_method_click_mouse(self, client: Kernel) -> None: ) assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_click_mouse_with_all_params(self, client: Kernel) -> None: computer = client.browsers.computer.click_mouse( @@ -166,7 +166,7 @@ def test_method_click_mouse_with_all_params(self, client: Kernel) -> None: ) assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_click_mouse(self, client: Kernel) -> None: response = client.browsers.computer.with_raw_response.click_mouse( @@ -180,7 +180,7 @@ def test_raw_response_click_mouse(self, client: Kernel) -> None: computer = response.parse() assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_click_mouse(self, client: Kernel) -> None: with client.browsers.computer.with_streaming_response.click_mouse( @@ -196,7 +196,7 @@ def test_streaming_response_click_mouse(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_click_mouse(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -206,7 +206,7 @@ def test_path_params_click_mouse(self, client: Kernel) -> None: y=0, ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_drag_mouse(self, client: Kernel) -> None: computer = client.browsers.computer.drag_mouse( @@ -215,7 +215,7 @@ def test_method_drag_mouse(self, client: Kernel) -> None: ) assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_drag_mouse_with_all_params(self, client: Kernel) -> None: computer = client.browsers.computer.drag_mouse( @@ -229,7 +229,7 @@ def test_method_drag_mouse_with_all_params(self, client: Kernel) -> None: ) assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_drag_mouse(self, client: Kernel) -> None: response = client.browsers.computer.with_raw_response.drag_mouse( @@ -242,7 +242,7 @@ def test_raw_response_drag_mouse(self, client: Kernel) -> None: computer = response.parse() assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_drag_mouse(self, client: Kernel) -> None: with client.browsers.computer.with_streaming_response.drag_mouse( @@ -257,7 +257,7 @@ def test_streaming_response_drag_mouse(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_drag_mouse(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -266,7 +266,7 @@ def test_path_params_drag_mouse(self, client: Kernel) -> None: path=[[0, 0], [0, 0]], ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_get_mouse_position(self, client: Kernel) -> None: computer = client.browsers.computer.get_mouse_position( @@ -274,7 +274,7 @@ def test_method_get_mouse_position(self, client: Kernel) -> None: ) assert_matches_type(ComputerGetMousePositionResponse, computer, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_get_mouse_position(self, client: Kernel) -> None: response = client.browsers.computer.with_raw_response.get_mouse_position( @@ -286,7 +286,7 @@ def test_raw_response_get_mouse_position(self, client: Kernel) -> None: computer = response.parse() assert_matches_type(ComputerGetMousePositionResponse, computer, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_get_mouse_position(self, client: Kernel) -> None: with client.browsers.computer.with_streaming_response.get_mouse_position( @@ -300,7 +300,7 @@ def test_streaming_response_get_mouse_position(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_get_mouse_position(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -308,7 +308,7 @@ def test_path_params_get_mouse_position(self, client: Kernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_move_mouse(self, client: Kernel) -> None: computer = client.browsers.computer.move_mouse( @@ -318,7 +318,7 @@ def test_method_move_mouse(self, client: Kernel) -> None: ) assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_move_mouse_with_all_params(self, client: Kernel) -> None: computer = client.browsers.computer.move_mouse( @@ -329,7 +329,7 @@ def test_method_move_mouse_with_all_params(self, client: Kernel) -> None: ) assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_move_mouse(self, client: Kernel) -> None: response = client.browsers.computer.with_raw_response.move_mouse( @@ -343,7 +343,7 @@ def test_raw_response_move_mouse(self, client: Kernel) -> None: computer = response.parse() assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_move_mouse(self, client: Kernel) -> None: with client.browsers.computer.with_streaming_response.move_mouse( @@ -359,7 +359,7 @@ def test_streaming_response_move_mouse(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_move_mouse(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -369,7 +369,7 @@ def test_path_params_move_mouse(self, client: Kernel) -> None: y=0, ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_press_key(self, client: Kernel) -> None: computer = client.browsers.computer.press_key( @@ -378,7 +378,7 @@ def test_method_press_key(self, client: Kernel) -> None: ) assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_press_key_with_all_params(self, client: Kernel) -> None: computer = client.browsers.computer.press_key( @@ -389,7 +389,7 @@ def test_method_press_key_with_all_params(self, client: Kernel) -> None: ) assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_press_key(self, client: Kernel) -> None: response = client.browsers.computer.with_raw_response.press_key( @@ -402,7 +402,7 @@ def test_raw_response_press_key(self, client: Kernel) -> None: computer = response.parse() assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_press_key(self, client: Kernel) -> None: with client.browsers.computer.with_streaming_response.press_key( @@ -417,7 +417,7 @@ def test_streaming_response_press_key(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_press_key(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -426,7 +426,7 @@ def test_path_params_press_key(self, client: Kernel) -> None: keys=["string"], ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_scroll(self, client: Kernel) -> None: computer = client.browsers.computer.scroll( @@ -436,7 +436,7 @@ def test_method_scroll(self, client: Kernel) -> None: ) assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_scroll_with_all_params(self, client: Kernel) -> None: computer = client.browsers.computer.scroll( @@ -449,7 +449,7 @@ def test_method_scroll_with_all_params(self, client: Kernel) -> None: ) assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_scroll(self, client: Kernel) -> None: response = client.browsers.computer.with_raw_response.scroll( @@ -463,7 +463,7 @@ def test_raw_response_scroll(self, client: Kernel) -> None: computer = response.parse() assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_scroll(self, client: Kernel) -> None: with client.browsers.computer.with_streaming_response.scroll( @@ -479,7 +479,7 @@ def test_streaming_response_scroll(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_scroll(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -489,7 +489,7 @@ def test_path_params_scroll(self, client: Kernel) -> None: y=0, ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_set_cursor_visibility(self, client: Kernel) -> None: computer = client.browsers.computer.set_cursor_visibility( @@ -498,7 +498,7 @@ def test_method_set_cursor_visibility(self, client: Kernel) -> None: ) assert_matches_type(ComputerSetCursorVisibilityResponse, computer, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_set_cursor_visibility(self, client: Kernel) -> None: response = client.browsers.computer.with_raw_response.set_cursor_visibility( @@ -511,7 +511,7 @@ def test_raw_response_set_cursor_visibility(self, client: Kernel) -> None: computer = response.parse() assert_matches_type(ComputerSetCursorVisibilityResponse, computer, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_set_cursor_visibility(self, client: Kernel) -> None: with client.browsers.computer.with_streaming_response.set_cursor_visibility( @@ -526,7 +526,7 @@ def test_streaming_response_set_cursor_visibility(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_set_cursor_visibility(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -535,7 +535,7 @@ def test_path_params_set_cursor_visibility(self, client: Kernel) -> None: hidden=True, ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_type_text(self, client: Kernel) -> None: computer = client.browsers.computer.type_text( @@ -544,7 +544,7 @@ def test_method_type_text(self, client: Kernel) -> None: ) assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_type_text_with_all_params(self, client: Kernel) -> None: computer = client.browsers.computer.type_text( @@ -554,7 +554,7 @@ def test_method_type_text_with_all_params(self, client: Kernel) -> None: ) assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_type_text(self, client: Kernel) -> None: response = client.browsers.computer.with_raw_response.type_text( @@ -567,7 +567,7 @@ def test_raw_response_type_text(self, client: Kernel) -> None: computer = response.parse() assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_type_text(self, client: Kernel) -> None: with client.browsers.computer.with_streaming_response.type_text( @@ -582,7 +582,7 @@ def test_streaming_response_type_text(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_type_text(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -597,7 +597,7 @@ class TestAsyncComputer: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_batch(self, async_client: AsyncKernel) -> None: computer = await async_client.browsers.computer.batch( @@ -606,7 +606,7 @@ async def test_method_batch(self, async_client: AsyncKernel) -> None: ) assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_batch(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.computer.with_raw_response.batch( @@ -619,7 +619,7 @@ async def test_raw_response_batch(self, async_client: AsyncKernel) -> None: computer = await response.parse() assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_batch(self, async_client: AsyncKernel) -> None: async with async_client.browsers.computer.with_streaming_response.batch( @@ -634,7 +634,7 @@ async def test_streaming_response_batch(self, async_client: AsyncKernel) -> None assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_batch(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -715,7 +715,7 @@ async def test_path_params_capture_screenshot(self, async_client: AsyncKernel) - id="", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_click_mouse(self, async_client: AsyncKernel) -> None: computer = await async_client.browsers.computer.click_mouse( @@ -725,7 +725,7 @@ async def test_method_click_mouse(self, async_client: AsyncKernel) -> None: ) assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_click_mouse_with_all_params(self, async_client: AsyncKernel) -> None: computer = await async_client.browsers.computer.click_mouse( @@ -739,7 +739,7 @@ async def test_method_click_mouse_with_all_params(self, async_client: AsyncKerne ) assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_click_mouse(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.computer.with_raw_response.click_mouse( @@ -753,7 +753,7 @@ async def test_raw_response_click_mouse(self, async_client: AsyncKernel) -> None computer = await response.parse() assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_click_mouse(self, async_client: AsyncKernel) -> None: async with async_client.browsers.computer.with_streaming_response.click_mouse( @@ -769,7 +769,7 @@ async def test_streaming_response_click_mouse(self, async_client: AsyncKernel) - assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_click_mouse(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -779,7 +779,7 @@ async def test_path_params_click_mouse(self, async_client: AsyncKernel) -> None: y=0, ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_drag_mouse(self, async_client: AsyncKernel) -> None: computer = await async_client.browsers.computer.drag_mouse( @@ -788,7 +788,7 @@ async def test_method_drag_mouse(self, async_client: AsyncKernel) -> None: ) assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_drag_mouse_with_all_params(self, async_client: AsyncKernel) -> None: computer = await async_client.browsers.computer.drag_mouse( @@ -802,7 +802,7 @@ async def test_method_drag_mouse_with_all_params(self, async_client: AsyncKernel ) assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_drag_mouse(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.computer.with_raw_response.drag_mouse( @@ -815,7 +815,7 @@ async def test_raw_response_drag_mouse(self, async_client: AsyncKernel) -> None: computer = await response.parse() assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_drag_mouse(self, async_client: AsyncKernel) -> None: async with async_client.browsers.computer.with_streaming_response.drag_mouse( @@ -830,7 +830,7 @@ async def test_streaming_response_drag_mouse(self, async_client: AsyncKernel) -> assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_drag_mouse(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -839,7 +839,7 @@ async def test_path_params_drag_mouse(self, async_client: AsyncKernel) -> None: path=[[0, 0], [0, 0]], ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_get_mouse_position(self, async_client: AsyncKernel) -> None: computer = await async_client.browsers.computer.get_mouse_position( @@ -847,7 +847,7 @@ async def test_method_get_mouse_position(self, async_client: AsyncKernel) -> Non ) assert_matches_type(ComputerGetMousePositionResponse, computer, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_get_mouse_position(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.computer.with_raw_response.get_mouse_position( @@ -859,7 +859,7 @@ async def test_raw_response_get_mouse_position(self, async_client: AsyncKernel) computer = await response.parse() assert_matches_type(ComputerGetMousePositionResponse, computer, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_get_mouse_position(self, async_client: AsyncKernel) -> None: async with async_client.browsers.computer.with_streaming_response.get_mouse_position( @@ -873,7 +873,7 @@ async def test_streaming_response_get_mouse_position(self, async_client: AsyncKe assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_get_mouse_position(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -881,7 +881,7 @@ async def test_path_params_get_mouse_position(self, async_client: AsyncKernel) - "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_move_mouse(self, async_client: AsyncKernel) -> None: computer = await async_client.browsers.computer.move_mouse( @@ -891,7 +891,7 @@ async def test_method_move_mouse(self, async_client: AsyncKernel) -> None: ) assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_move_mouse_with_all_params(self, async_client: AsyncKernel) -> None: computer = await async_client.browsers.computer.move_mouse( @@ -902,7 +902,7 @@ async def test_method_move_mouse_with_all_params(self, async_client: AsyncKernel ) assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_move_mouse(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.computer.with_raw_response.move_mouse( @@ -916,7 +916,7 @@ async def test_raw_response_move_mouse(self, async_client: AsyncKernel) -> None: computer = await response.parse() assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_move_mouse(self, async_client: AsyncKernel) -> None: async with async_client.browsers.computer.with_streaming_response.move_mouse( @@ -932,7 +932,7 @@ async def test_streaming_response_move_mouse(self, async_client: AsyncKernel) -> assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_move_mouse(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -942,7 +942,7 @@ async def test_path_params_move_mouse(self, async_client: AsyncKernel) -> None: y=0, ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_press_key(self, async_client: AsyncKernel) -> None: computer = await async_client.browsers.computer.press_key( @@ -951,7 +951,7 @@ async def test_method_press_key(self, async_client: AsyncKernel) -> None: ) assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_press_key_with_all_params(self, async_client: AsyncKernel) -> None: computer = await async_client.browsers.computer.press_key( @@ -962,7 +962,7 @@ async def test_method_press_key_with_all_params(self, async_client: AsyncKernel) ) assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_press_key(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.computer.with_raw_response.press_key( @@ -975,7 +975,7 @@ async def test_raw_response_press_key(self, async_client: AsyncKernel) -> None: computer = await response.parse() assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_press_key(self, async_client: AsyncKernel) -> None: async with async_client.browsers.computer.with_streaming_response.press_key( @@ -990,7 +990,7 @@ async def test_streaming_response_press_key(self, async_client: AsyncKernel) -> assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_press_key(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -999,7 +999,7 @@ async def test_path_params_press_key(self, async_client: AsyncKernel) -> None: keys=["string"], ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_scroll(self, async_client: AsyncKernel) -> None: computer = await async_client.browsers.computer.scroll( @@ -1009,7 +1009,7 @@ async def test_method_scroll(self, async_client: AsyncKernel) -> None: ) assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_scroll_with_all_params(self, async_client: AsyncKernel) -> None: computer = await async_client.browsers.computer.scroll( @@ -1022,7 +1022,7 @@ async def test_method_scroll_with_all_params(self, async_client: AsyncKernel) -> ) assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_scroll(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.computer.with_raw_response.scroll( @@ -1036,7 +1036,7 @@ async def test_raw_response_scroll(self, async_client: AsyncKernel) -> None: computer = await response.parse() assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_scroll(self, async_client: AsyncKernel) -> None: async with async_client.browsers.computer.with_streaming_response.scroll( @@ -1052,7 +1052,7 @@ async def test_streaming_response_scroll(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_scroll(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -1062,7 +1062,7 @@ async def test_path_params_scroll(self, async_client: AsyncKernel) -> None: y=0, ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_set_cursor_visibility(self, async_client: AsyncKernel) -> None: computer = await async_client.browsers.computer.set_cursor_visibility( @@ -1071,7 +1071,7 @@ async def test_method_set_cursor_visibility(self, async_client: AsyncKernel) -> ) assert_matches_type(ComputerSetCursorVisibilityResponse, computer, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_set_cursor_visibility(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.computer.with_raw_response.set_cursor_visibility( @@ -1084,7 +1084,7 @@ async def test_raw_response_set_cursor_visibility(self, async_client: AsyncKerne computer = await response.parse() assert_matches_type(ComputerSetCursorVisibilityResponse, computer, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_set_cursor_visibility(self, async_client: AsyncKernel) -> None: async with async_client.browsers.computer.with_streaming_response.set_cursor_visibility( @@ -1099,7 +1099,7 @@ async def test_streaming_response_set_cursor_visibility(self, async_client: Asyn assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_set_cursor_visibility(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -1108,7 +1108,7 @@ async def test_path_params_set_cursor_visibility(self, async_client: AsyncKernel hidden=True, ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_type_text(self, async_client: AsyncKernel) -> None: computer = await async_client.browsers.computer.type_text( @@ -1117,7 +1117,7 @@ async def test_method_type_text(self, async_client: AsyncKernel) -> None: ) assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_type_text_with_all_params(self, async_client: AsyncKernel) -> None: computer = await async_client.browsers.computer.type_text( @@ -1127,7 +1127,7 @@ async def test_method_type_text_with_all_params(self, async_client: AsyncKernel) ) assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_type_text(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.computer.with_raw_response.type_text( @@ -1140,7 +1140,7 @@ async def test_raw_response_type_text(self, async_client: AsyncKernel) -> None: computer = await response.parse() assert computer is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_type_text(self, async_client: AsyncKernel) -> None: async with async_client.browsers.computer.with_streaming_response.type_text( @@ -1155,7 +1155,7 @@ async def test_streaming_response_type_text(self, async_client: AsyncKernel) -> assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_type_text(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): diff --git a/tests/api_resources/browsers/test_fs.py b/tests/api_resources/browsers/test_fs.py index 38e07b78..1d314e3a 100644 --- a/tests/api_resources/browsers/test_fs.py +++ b/tests/api_resources/browsers/test_fs.py @@ -28,7 +28,7 @@ class TestFs: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create_directory(self, client: Kernel) -> None: f = client.browsers.fs.create_directory( @@ -37,7 +37,7 @@ def test_method_create_directory(self, client: Kernel) -> None: ) assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create_directory_with_all_params(self, client: Kernel) -> None: f = client.browsers.fs.create_directory( @@ -47,7 +47,7 @@ def test_method_create_directory_with_all_params(self, client: Kernel) -> None: ) assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_create_directory(self, client: Kernel) -> None: response = client.browsers.fs.with_raw_response.create_directory( @@ -60,7 +60,7 @@ def test_raw_response_create_directory(self, client: Kernel) -> None: f = response.parse() assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_create_directory(self, client: Kernel) -> None: with client.browsers.fs.with_streaming_response.create_directory( @@ -75,7 +75,7 @@ def test_streaming_response_create_directory(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_create_directory(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -84,7 +84,7 @@ def test_path_params_create_directory(self, client: Kernel) -> None: path="/J!", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_delete_directory(self, client: Kernel) -> None: f = client.browsers.fs.delete_directory( @@ -93,7 +93,7 @@ def test_method_delete_directory(self, client: Kernel) -> None: ) assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_delete_directory(self, client: Kernel) -> None: response = client.browsers.fs.with_raw_response.delete_directory( @@ -106,7 +106,7 @@ def test_raw_response_delete_directory(self, client: Kernel) -> None: f = response.parse() assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_delete_directory(self, client: Kernel) -> None: with client.browsers.fs.with_streaming_response.delete_directory( @@ -121,7 +121,7 @@ def test_streaming_response_delete_directory(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_delete_directory(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -130,7 +130,7 @@ def test_path_params_delete_directory(self, client: Kernel) -> None: path="/J!", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_delete_file(self, client: Kernel) -> None: f = client.browsers.fs.delete_file( @@ -139,7 +139,7 @@ def test_method_delete_file(self, client: Kernel) -> None: ) assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_delete_file(self, client: Kernel) -> None: response = client.browsers.fs.with_raw_response.delete_file( @@ -152,7 +152,7 @@ def test_raw_response_delete_file(self, client: Kernel) -> None: f = response.parse() assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_delete_file(self, client: Kernel) -> None: with client.browsers.fs.with_streaming_response.delete_file( @@ -167,7 +167,7 @@ def test_streaming_response_delete_file(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_delete_file(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -230,7 +230,7 @@ def test_path_params_download_dir_zip(self, client: Kernel) -> None: path="/J!", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_file_info(self, client: Kernel) -> None: f = client.browsers.fs.file_info( @@ -239,7 +239,7 @@ def test_method_file_info(self, client: Kernel) -> None: ) assert_matches_type(FFileInfoResponse, f, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_file_info(self, client: Kernel) -> None: response = client.browsers.fs.with_raw_response.file_info( @@ -252,7 +252,7 @@ def test_raw_response_file_info(self, client: Kernel) -> None: f = response.parse() assert_matches_type(FFileInfoResponse, f, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_file_info(self, client: Kernel) -> None: with client.browsers.fs.with_streaming_response.file_info( @@ -267,7 +267,7 @@ def test_streaming_response_file_info(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_file_info(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -276,7 +276,7 @@ def test_path_params_file_info(self, client: Kernel) -> None: path="/J!", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_list_files(self, client: Kernel) -> None: f = client.browsers.fs.list_files( @@ -285,7 +285,7 @@ def test_method_list_files(self, client: Kernel) -> None: ) assert_matches_type(FListFilesResponse, f, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_list_files(self, client: Kernel) -> None: response = client.browsers.fs.with_raw_response.list_files( @@ -298,7 +298,7 @@ def test_raw_response_list_files(self, client: Kernel) -> None: f = response.parse() assert_matches_type(FListFilesResponse, f, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_list_files(self, client: Kernel) -> None: with client.browsers.fs.with_streaming_response.list_files( @@ -313,7 +313,7 @@ def test_streaming_response_list_files(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_list_files(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -322,7 +322,7 @@ def test_path_params_list_files(self, client: Kernel) -> None: path="/J!", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_move(self, client: Kernel) -> None: f = client.browsers.fs.move( @@ -332,7 +332,7 @@ def test_method_move(self, client: Kernel) -> None: ) assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_move(self, client: Kernel) -> None: response = client.browsers.fs.with_raw_response.move( @@ -346,7 +346,7 @@ def test_raw_response_move(self, client: Kernel) -> None: f = response.parse() assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_move(self, client: Kernel) -> None: with client.browsers.fs.with_streaming_response.move( @@ -362,7 +362,7 @@ def test_streaming_response_move(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_move(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -426,7 +426,7 @@ def test_path_params_read_file(self, client: Kernel) -> None: path="/J!", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_set_file_permissions(self, client: Kernel) -> None: f = client.browsers.fs.set_file_permissions( @@ -436,7 +436,7 @@ def test_method_set_file_permissions(self, client: Kernel) -> None: ) assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_set_file_permissions_with_all_params(self, client: Kernel) -> None: f = client.browsers.fs.set_file_permissions( @@ -448,7 +448,7 @@ def test_method_set_file_permissions_with_all_params(self, client: Kernel) -> No ) assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_set_file_permissions(self, client: Kernel) -> None: response = client.browsers.fs.with_raw_response.set_file_permissions( @@ -462,7 +462,7 @@ def test_raw_response_set_file_permissions(self, client: Kernel) -> None: f = response.parse() assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_set_file_permissions(self, client: Kernel) -> None: with client.browsers.fs.with_streaming_response.set_file_permissions( @@ -478,7 +478,7 @@ def test_streaming_response_set_file_permissions(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_set_file_permissions(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -488,7 +488,7 @@ def test_path_params_set_file_permissions(self, client: Kernel) -> None: path="/J!", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_upload(self, client: Kernel) -> None: f = client.browsers.fs.upload( @@ -502,7 +502,7 @@ def test_method_upload(self, client: Kernel) -> None: ) assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_upload(self, client: Kernel) -> None: response = client.browsers.fs.with_raw_response.upload( @@ -520,7 +520,7 @@ def test_raw_response_upload(self, client: Kernel) -> None: f = response.parse() assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_upload(self, client: Kernel) -> None: with client.browsers.fs.with_streaming_response.upload( @@ -540,7 +540,7 @@ def test_streaming_response_upload(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_upload(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -554,7 +554,7 @@ def test_path_params_upload(self, client: Kernel) -> None: ], ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_upload_zip(self, client: Kernel) -> None: f = client.browsers.fs.upload_zip( @@ -564,7 +564,7 @@ def test_method_upload_zip(self, client: Kernel) -> None: ) assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_upload_zip(self, client: Kernel) -> None: response = client.browsers.fs.with_raw_response.upload_zip( @@ -578,7 +578,7 @@ def test_raw_response_upload_zip(self, client: Kernel) -> None: f = response.parse() assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_upload_zip(self, client: Kernel) -> None: with client.browsers.fs.with_streaming_response.upload_zip( @@ -594,7 +594,7 @@ def test_streaming_response_upload_zip(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_upload_zip(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -604,7 +604,7 @@ def test_path_params_upload_zip(self, client: Kernel) -> None: zip_file=b"raw file contents", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_write_file(self, client: Kernel) -> None: f = client.browsers.fs.write_file( @@ -614,7 +614,7 @@ def test_method_write_file(self, client: Kernel) -> None: ) assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_write_file_with_all_params(self, client: Kernel) -> None: f = client.browsers.fs.write_file( @@ -625,7 +625,7 @@ def test_method_write_file_with_all_params(self, client: Kernel) -> None: ) assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_write_file(self, client: Kernel) -> None: response = client.browsers.fs.with_raw_response.write_file( @@ -639,7 +639,7 @@ def test_raw_response_write_file(self, client: Kernel) -> None: f = response.parse() assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_write_file(self, client: Kernel) -> None: with client.browsers.fs.with_streaming_response.write_file( @@ -655,7 +655,7 @@ def test_streaming_response_write_file(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_write_file(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -671,7 +671,7 @@ class TestAsyncFs: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create_directory(self, async_client: AsyncKernel) -> None: f = await async_client.browsers.fs.create_directory( @@ -680,7 +680,7 @@ async def test_method_create_directory(self, async_client: AsyncKernel) -> None: ) assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create_directory_with_all_params(self, async_client: AsyncKernel) -> None: f = await async_client.browsers.fs.create_directory( @@ -690,7 +690,7 @@ async def test_method_create_directory_with_all_params(self, async_client: Async ) assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_create_directory(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.fs.with_raw_response.create_directory( @@ -703,7 +703,7 @@ async def test_raw_response_create_directory(self, async_client: AsyncKernel) -> f = await response.parse() assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_create_directory(self, async_client: AsyncKernel) -> None: async with async_client.browsers.fs.with_streaming_response.create_directory( @@ -718,7 +718,7 @@ async def test_streaming_response_create_directory(self, async_client: AsyncKern assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_create_directory(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -727,7 +727,7 @@ async def test_path_params_create_directory(self, async_client: AsyncKernel) -> path="/J!", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_delete_directory(self, async_client: AsyncKernel) -> None: f = await async_client.browsers.fs.delete_directory( @@ -736,7 +736,7 @@ async def test_method_delete_directory(self, async_client: AsyncKernel) -> None: ) assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_delete_directory(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.fs.with_raw_response.delete_directory( @@ -749,7 +749,7 @@ async def test_raw_response_delete_directory(self, async_client: AsyncKernel) -> f = await response.parse() assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_delete_directory(self, async_client: AsyncKernel) -> None: async with async_client.browsers.fs.with_streaming_response.delete_directory( @@ -764,7 +764,7 @@ async def test_streaming_response_delete_directory(self, async_client: AsyncKern assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_delete_directory(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -773,7 +773,7 @@ async def test_path_params_delete_directory(self, async_client: AsyncKernel) -> path="/J!", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_delete_file(self, async_client: AsyncKernel) -> None: f = await async_client.browsers.fs.delete_file( @@ -782,7 +782,7 @@ async def test_method_delete_file(self, async_client: AsyncKernel) -> None: ) assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_delete_file(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.fs.with_raw_response.delete_file( @@ -795,7 +795,7 @@ async def test_raw_response_delete_file(self, async_client: AsyncKernel) -> None f = await response.parse() assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_delete_file(self, async_client: AsyncKernel) -> None: async with async_client.browsers.fs.with_streaming_response.delete_file( @@ -810,7 +810,7 @@ async def test_streaming_response_delete_file(self, async_client: AsyncKernel) - assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_delete_file(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -873,7 +873,7 @@ async def test_path_params_download_dir_zip(self, async_client: AsyncKernel) -> path="/J!", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_file_info(self, async_client: AsyncKernel) -> None: f = await async_client.browsers.fs.file_info( @@ -882,7 +882,7 @@ async def test_method_file_info(self, async_client: AsyncKernel) -> None: ) assert_matches_type(FFileInfoResponse, f, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_file_info(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.fs.with_raw_response.file_info( @@ -895,7 +895,7 @@ async def test_raw_response_file_info(self, async_client: AsyncKernel) -> None: f = await response.parse() assert_matches_type(FFileInfoResponse, f, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_file_info(self, async_client: AsyncKernel) -> None: async with async_client.browsers.fs.with_streaming_response.file_info( @@ -910,7 +910,7 @@ async def test_streaming_response_file_info(self, async_client: AsyncKernel) -> assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_file_info(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -919,7 +919,7 @@ async def test_path_params_file_info(self, async_client: AsyncKernel) -> None: path="/J!", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_list_files(self, async_client: AsyncKernel) -> None: f = await async_client.browsers.fs.list_files( @@ -928,7 +928,7 @@ async def test_method_list_files(self, async_client: AsyncKernel) -> None: ) assert_matches_type(FListFilesResponse, f, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_list_files(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.fs.with_raw_response.list_files( @@ -941,7 +941,7 @@ async def test_raw_response_list_files(self, async_client: AsyncKernel) -> None: f = await response.parse() assert_matches_type(FListFilesResponse, f, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_list_files(self, async_client: AsyncKernel) -> None: async with async_client.browsers.fs.with_streaming_response.list_files( @@ -956,7 +956,7 @@ async def test_streaming_response_list_files(self, async_client: AsyncKernel) -> assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_list_files(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -965,7 +965,7 @@ async def test_path_params_list_files(self, async_client: AsyncKernel) -> None: path="/J!", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_move(self, async_client: AsyncKernel) -> None: f = await async_client.browsers.fs.move( @@ -975,7 +975,7 @@ async def test_method_move(self, async_client: AsyncKernel) -> None: ) assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_move(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.fs.with_raw_response.move( @@ -989,7 +989,7 @@ async def test_raw_response_move(self, async_client: AsyncKernel) -> None: f = await response.parse() assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_move(self, async_client: AsyncKernel) -> None: async with async_client.browsers.fs.with_streaming_response.move( @@ -1005,7 +1005,7 @@ async def test_streaming_response_move(self, async_client: AsyncKernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_move(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -1069,7 +1069,7 @@ async def test_path_params_read_file(self, async_client: AsyncKernel) -> None: path="/J!", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_set_file_permissions(self, async_client: AsyncKernel) -> None: f = await async_client.browsers.fs.set_file_permissions( @@ -1079,7 +1079,7 @@ async def test_method_set_file_permissions(self, async_client: AsyncKernel) -> N ) assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_set_file_permissions_with_all_params(self, async_client: AsyncKernel) -> None: f = await async_client.browsers.fs.set_file_permissions( @@ -1091,7 +1091,7 @@ async def test_method_set_file_permissions_with_all_params(self, async_client: A ) assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_set_file_permissions(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.fs.with_raw_response.set_file_permissions( @@ -1105,7 +1105,7 @@ async def test_raw_response_set_file_permissions(self, async_client: AsyncKernel f = await response.parse() assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_set_file_permissions(self, async_client: AsyncKernel) -> None: async with async_client.browsers.fs.with_streaming_response.set_file_permissions( @@ -1121,7 +1121,7 @@ async def test_streaming_response_set_file_permissions(self, async_client: Async assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_set_file_permissions(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -1131,7 +1131,7 @@ async def test_path_params_set_file_permissions(self, async_client: AsyncKernel) path="/J!", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_upload(self, async_client: AsyncKernel) -> None: f = await async_client.browsers.fs.upload( @@ -1145,7 +1145,7 @@ async def test_method_upload(self, async_client: AsyncKernel) -> None: ) assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_upload(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.fs.with_raw_response.upload( @@ -1163,7 +1163,7 @@ async def test_raw_response_upload(self, async_client: AsyncKernel) -> None: f = await response.parse() assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_upload(self, async_client: AsyncKernel) -> None: async with async_client.browsers.fs.with_streaming_response.upload( @@ -1183,7 +1183,7 @@ async def test_streaming_response_upload(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_upload(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -1197,7 +1197,7 @@ async def test_path_params_upload(self, async_client: AsyncKernel) -> None: ], ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_upload_zip(self, async_client: AsyncKernel) -> None: f = await async_client.browsers.fs.upload_zip( @@ -1207,7 +1207,7 @@ async def test_method_upload_zip(self, async_client: AsyncKernel) -> None: ) assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_upload_zip(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.fs.with_raw_response.upload_zip( @@ -1221,7 +1221,7 @@ async def test_raw_response_upload_zip(self, async_client: AsyncKernel) -> None: f = await response.parse() assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_upload_zip(self, async_client: AsyncKernel) -> None: async with async_client.browsers.fs.with_streaming_response.upload_zip( @@ -1237,7 +1237,7 @@ async def test_streaming_response_upload_zip(self, async_client: AsyncKernel) -> assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_upload_zip(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -1247,7 +1247,7 @@ async def test_path_params_upload_zip(self, async_client: AsyncKernel) -> None: zip_file=b"raw file contents", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_write_file(self, async_client: AsyncKernel) -> None: f = await async_client.browsers.fs.write_file( @@ -1257,7 +1257,7 @@ async def test_method_write_file(self, async_client: AsyncKernel) -> None: ) assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_write_file_with_all_params(self, async_client: AsyncKernel) -> None: f = await async_client.browsers.fs.write_file( @@ -1268,7 +1268,7 @@ async def test_method_write_file_with_all_params(self, async_client: AsyncKernel ) assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_write_file(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.fs.with_raw_response.write_file( @@ -1282,7 +1282,7 @@ async def test_raw_response_write_file(self, async_client: AsyncKernel) -> None: f = await response.parse() assert f is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_write_file(self, async_client: AsyncKernel) -> None: async with async_client.browsers.fs.with_streaming_response.write_file( @@ -1298,7 +1298,7 @@ async def test_streaming_response_write_file(self, async_client: AsyncKernel) -> assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_write_file(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): diff --git a/tests/api_resources/browsers/test_logs.py b/tests/api_resources/browsers/test_logs.py index 6aac62f6..8048908d 100644 --- a/tests/api_resources/browsers/test_logs.py +++ b/tests/api_resources/browsers/test_logs.py @@ -15,7 +15,7 @@ class TestLogs: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize def test_method_stream(self, client: Kernel) -> None: log_stream = client.browsers.logs.stream( @@ -24,7 +24,7 @@ def test_method_stream(self, client: Kernel) -> None: ) log_stream.response.close() - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize def test_method_stream_with_all_params(self, client: Kernel) -> None: log_stream = client.browsers.logs.stream( @@ -36,7 +36,7 @@ def test_method_stream_with_all_params(self, client: Kernel) -> None: ) log_stream.response.close() - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize def test_raw_response_stream(self, client: Kernel) -> None: response = client.browsers.logs.with_raw_response.stream( @@ -48,7 +48,7 @@ def test_raw_response_stream(self, client: Kernel) -> None: stream = response.parse() stream.close() - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize def test_streaming_response_stream(self, client: Kernel) -> None: with client.browsers.logs.with_streaming_response.stream( @@ -63,7 +63,7 @@ def test_streaming_response_stream(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize def test_path_params_stream(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -78,7 +78,7 @@ class TestAsyncLogs: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize async def test_method_stream(self, async_client: AsyncKernel) -> None: log_stream = await async_client.browsers.logs.stream( @@ -87,7 +87,7 @@ async def test_method_stream(self, async_client: AsyncKernel) -> None: ) await log_stream.response.aclose() - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize async def test_method_stream_with_all_params(self, async_client: AsyncKernel) -> None: log_stream = await async_client.browsers.logs.stream( @@ -99,7 +99,7 @@ async def test_method_stream_with_all_params(self, async_client: AsyncKernel) -> ) await log_stream.response.aclose() - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize async def test_raw_response_stream(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.logs.with_raw_response.stream( @@ -111,7 +111,7 @@ async def test_raw_response_stream(self, async_client: AsyncKernel) -> None: stream = await response.parse() await stream.close() - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize async def test_streaming_response_stream(self, async_client: AsyncKernel) -> None: async with async_client.browsers.logs.with_streaming_response.stream( @@ -126,7 +126,7 @@ async def test_streaming_response_stream(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize async def test_path_params_stream(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): diff --git a/tests/api_resources/browsers/test_playwright.py b/tests/api_resources/browsers/test_playwright.py index cb79410c..fd91b832 100644 --- a/tests/api_resources/browsers/test_playwright.py +++ b/tests/api_resources/browsers/test_playwright.py @@ -17,7 +17,7 @@ class TestPlaywright: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_execute(self, client: Kernel) -> None: playwright = client.browsers.playwright.execute( @@ -26,7 +26,7 @@ def test_method_execute(self, client: Kernel) -> None: ) assert_matches_type(PlaywrightExecuteResponse, playwright, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_execute_with_all_params(self, client: Kernel) -> None: playwright = client.browsers.playwright.execute( @@ -36,7 +36,7 @@ def test_method_execute_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(PlaywrightExecuteResponse, playwright, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_execute(self, client: Kernel) -> None: response = client.browsers.playwright.with_raw_response.execute( @@ -49,7 +49,7 @@ def test_raw_response_execute(self, client: Kernel) -> None: playwright = response.parse() assert_matches_type(PlaywrightExecuteResponse, playwright, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_execute(self, client: Kernel) -> None: with client.browsers.playwright.with_streaming_response.execute( @@ -64,7 +64,7 @@ def test_streaming_response_execute(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_execute(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -79,7 +79,7 @@ class TestAsyncPlaywright: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_execute(self, async_client: AsyncKernel) -> None: playwright = await async_client.browsers.playwright.execute( @@ -88,7 +88,7 @@ async def test_method_execute(self, async_client: AsyncKernel) -> None: ) assert_matches_type(PlaywrightExecuteResponse, playwright, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_execute_with_all_params(self, async_client: AsyncKernel) -> None: playwright = await async_client.browsers.playwright.execute( @@ -98,7 +98,7 @@ async def test_method_execute_with_all_params(self, async_client: AsyncKernel) - ) assert_matches_type(PlaywrightExecuteResponse, playwright, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_execute(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.playwright.with_raw_response.execute( @@ -111,7 +111,7 @@ async def test_raw_response_execute(self, async_client: AsyncKernel) -> None: playwright = await response.parse() assert_matches_type(PlaywrightExecuteResponse, playwright, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_execute(self, async_client: AsyncKernel) -> None: async with async_client.browsers.playwright.with_streaming_response.execute( @@ -126,7 +126,7 @@ async def test_streaming_response_execute(self, async_client: AsyncKernel) -> No assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_execute(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): diff --git a/tests/api_resources/browsers/test_process.py b/tests/api_resources/browsers/test_process.py index 3c645fa4..1fc71335 100644 --- a/tests/api_resources/browsers/test_process.py +++ b/tests/api_resources/browsers/test_process.py @@ -24,7 +24,7 @@ class TestProcess: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_exec(self, client: Kernel) -> None: process = client.browsers.process.exec( @@ -33,7 +33,7 @@ def test_method_exec(self, client: Kernel) -> None: ) assert_matches_type(ProcessExecResponse, process, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_exec_with_all_params(self, client: Kernel) -> None: process = client.browsers.process.exec( @@ -48,7 +48,7 @@ def test_method_exec_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(ProcessExecResponse, process, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_exec(self, client: Kernel) -> None: response = client.browsers.process.with_raw_response.exec( @@ -61,7 +61,7 @@ def test_raw_response_exec(self, client: Kernel) -> None: process = response.parse() assert_matches_type(ProcessExecResponse, process, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_exec(self, client: Kernel) -> None: with client.browsers.process.with_streaming_response.exec( @@ -76,7 +76,7 @@ def test_streaming_response_exec(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_exec(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -85,7 +85,7 @@ def test_path_params_exec(self, client: Kernel) -> None: command="command", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_kill(self, client: Kernel) -> None: process = client.browsers.process.kill( @@ -95,7 +95,7 @@ def test_method_kill(self, client: Kernel) -> None: ) assert_matches_type(ProcessKillResponse, process, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_kill(self, client: Kernel) -> None: response = client.browsers.process.with_raw_response.kill( @@ -109,7 +109,7 @@ def test_raw_response_kill(self, client: Kernel) -> None: process = response.parse() assert_matches_type(ProcessKillResponse, process, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_kill(self, client: Kernel) -> None: with client.browsers.process.with_streaming_response.kill( @@ -125,7 +125,7 @@ def test_streaming_response_kill(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_kill(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -142,7 +142,7 @@ def test_path_params_kill(self, client: Kernel) -> None: signal="TERM", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_resize(self, client: Kernel) -> None: process = client.browsers.process.resize( @@ -153,7 +153,7 @@ def test_method_resize(self, client: Kernel) -> None: ) assert_matches_type(ProcessResizeResponse, process, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_resize(self, client: Kernel) -> None: response = client.browsers.process.with_raw_response.resize( @@ -168,7 +168,7 @@ def test_raw_response_resize(self, client: Kernel) -> None: process = response.parse() assert_matches_type(ProcessResizeResponse, process, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_resize(self, client: Kernel) -> None: with client.browsers.process.with_streaming_response.resize( @@ -185,7 +185,7 @@ def test_streaming_response_resize(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_resize(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -204,7 +204,7 @@ def test_path_params_resize(self, client: Kernel) -> None: rows=1, ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_spawn(self, client: Kernel) -> None: process = client.browsers.process.spawn( @@ -213,7 +213,7 @@ def test_method_spawn(self, client: Kernel) -> None: ) assert_matches_type(ProcessSpawnResponse, process, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_spawn_with_all_params(self, client: Kernel) -> None: process = client.browsers.process.spawn( @@ -231,7 +231,7 @@ def test_method_spawn_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(ProcessSpawnResponse, process, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_spawn(self, client: Kernel) -> None: response = client.browsers.process.with_raw_response.spawn( @@ -244,7 +244,7 @@ def test_raw_response_spawn(self, client: Kernel) -> None: process = response.parse() assert_matches_type(ProcessSpawnResponse, process, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_spawn(self, client: Kernel) -> None: with client.browsers.process.with_streaming_response.spawn( @@ -259,7 +259,7 @@ def test_streaming_response_spawn(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_spawn(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -268,7 +268,7 @@ def test_path_params_spawn(self, client: Kernel) -> None: command="command", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_status(self, client: Kernel) -> None: process = client.browsers.process.status( @@ -277,7 +277,7 @@ def test_method_status(self, client: Kernel) -> None: ) assert_matches_type(ProcessStatusResponse, process, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_status(self, client: Kernel) -> None: response = client.browsers.process.with_raw_response.status( @@ -290,7 +290,7 @@ def test_raw_response_status(self, client: Kernel) -> None: process = response.parse() assert_matches_type(ProcessStatusResponse, process, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_status(self, client: Kernel) -> None: with client.browsers.process.with_streaming_response.status( @@ -305,7 +305,7 @@ def test_streaming_response_status(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_status(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -320,7 +320,7 @@ def test_path_params_status(self, client: Kernel) -> None: id="id", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_stdin(self, client: Kernel) -> None: process = client.browsers.process.stdin( @@ -330,7 +330,7 @@ def test_method_stdin(self, client: Kernel) -> None: ) assert_matches_type(ProcessStdinResponse, process, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_stdin(self, client: Kernel) -> None: response = client.browsers.process.with_raw_response.stdin( @@ -344,7 +344,7 @@ def test_raw_response_stdin(self, client: Kernel) -> None: process = response.parse() assert_matches_type(ProcessStdinResponse, process, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_stdin(self, client: Kernel) -> None: with client.browsers.process.with_streaming_response.stdin( @@ -360,7 +360,7 @@ def test_streaming_response_stdin(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_stdin(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -377,7 +377,7 @@ def test_path_params_stdin(self, client: Kernel) -> None: data_b64="data_b64", ) - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize def test_method_stdout_stream(self, client: Kernel) -> None: process_stream = client.browsers.process.stdout_stream( @@ -386,7 +386,7 @@ def test_method_stdout_stream(self, client: Kernel) -> None: ) process_stream.response.close() - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize def test_raw_response_stdout_stream(self, client: Kernel) -> None: response = client.browsers.process.with_raw_response.stdout_stream( @@ -398,7 +398,7 @@ def test_raw_response_stdout_stream(self, client: Kernel) -> None: stream = response.parse() stream.close() - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize def test_streaming_response_stdout_stream(self, client: Kernel) -> None: with client.browsers.process.with_streaming_response.stdout_stream( @@ -413,7 +413,7 @@ def test_streaming_response_stdout_stream(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize def test_path_params_stdout_stream(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -434,7 +434,7 @@ class TestAsyncProcess: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_exec(self, async_client: AsyncKernel) -> None: process = await async_client.browsers.process.exec( @@ -443,7 +443,7 @@ async def test_method_exec(self, async_client: AsyncKernel) -> None: ) assert_matches_type(ProcessExecResponse, process, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_exec_with_all_params(self, async_client: AsyncKernel) -> None: process = await async_client.browsers.process.exec( @@ -458,7 +458,7 @@ async def test_method_exec_with_all_params(self, async_client: AsyncKernel) -> N ) assert_matches_type(ProcessExecResponse, process, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_exec(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.process.with_raw_response.exec( @@ -471,7 +471,7 @@ async def test_raw_response_exec(self, async_client: AsyncKernel) -> None: process = await response.parse() assert_matches_type(ProcessExecResponse, process, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_exec(self, async_client: AsyncKernel) -> None: async with async_client.browsers.process.with_streaming_response.exec( @@ -486,7 +486,7 @@ async def test_streaming_response_exec(self, async_client: AsyncKernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_exec(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -495,7 +495,7 @@ async def test_path_params_exec(self, async_client: AsyncKernel) -> None: command="command", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_kill(self, async_client: AsyncKernel) -> None: process = await async_client.browsers.process.kill( @@ -505,7 +505,7 @@ async def test_method_kill(self, async_client: AsyncKernel) -> None: ) assert_matches_type(ProcessKillResponse, process, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_kill(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.process.with_raw_response.kill( @@ -519,7 +519,7 @@ async def test_raw_response_kill(self, async_client: AsyncKernel) -> None: process = await response.parse() assert_matches_type(ProcessKillResponse, process, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_kill(self, async_client: AsyncKernel) -> None: async with async_client.browsers.process.with_streaming_response.kill( @@ -535,7 +535,7 @@ async def test_streaming_response_kill(self, async_client: AsyncKernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_kill(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -552,7 +552,7 @@ async def test_path_params_kill(self, async_client: AsyncKernel) -> None: signal="TERM", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_resize(self, async_client: AsyncKernel) -> None: process = await async_client.browsers.process.resize( @@ -563,7 +563,7 @@ async def test_method_resize(self, async_client: AsyncKernel) -> None: ) assert_matches_type(ProcessResizeResponse, process, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_resize(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.process.with_raw_response.resize( @@ -578,7 +578,7 @@ async def test_raw_response_resize(self, async_client: AsyncKernel) -> None: process = await response.parse() assert_matches_type(ProcessResizeResponse, process, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_resize(self, async_client: AsyncKernel) -> None: async with async_client.browsers.process.with_streaming_response.resize( @@ -595,7 +595,7 @@ async def test_streaming_response_resize(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_resize(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -614,7 +614,7 @@ async def test_path_params_resize(self, async_client: AsyncKernel) -> None: rows=1, ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_spawn(self, async_client: AsyncKernel) -> None: process = await async_client.browsers.process.spawn( @@ -623,7 +623,7 @@ async def test_method_spawn(self, async_client: AsyncKernel) -> None: ) assert_matches_type(ProcessSpawnResponse, process, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_spawn_with_all_params(self, async_client: AsyncKernel) -> None: process = await async_client.browsers.process.spawn( @@ -641,7 +641,7 @@ async def test_method_spawn_with_all_params(self, async_client: AsyncKernel) -> ) assert_matches_type(ProcessSpawnResponse, process, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_spawn(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.process.with_raw_response.spawn( @@ -654,7 +654,7 @@ async def test_raw_response_spawn(self, async_client: AsyncKernel) -> None: process = await response.parse() assert_matches_type(ProcessSpawnResponse, process, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_spawn(self, async_client: AsyncKernel) -> None: async with async_client.browsers.process.with_streaming_response.spawn( @@ -669,7 +669,7 @@ async def test_streaming_response_spawn(self, async_client: AsyncKernel) -> None assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_spawn(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -678,7 +678,7 @@ async def test_path_params_spawn(self, async_client: AsyncKernel) -> None: command="command", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_status(self, async_client: AsyncKernel) -> None: process = await async_client.browsers.process.status( @@ -687,7 +687,7 @@ async def test_method_status(self, async_client: AsyncKernel) -> None: ) assert_matches_type(ProcessStatusResponse, process, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_status(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.process.with_raw_response.status( @@ -700,7 +700,7 @@ async def test_raw_response_status(self, async_client: AsyncKernel) -> None: process = await response.parse() assert_matches_type(ProcessStatusResponse, process, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_status(self, async_client: AsyncKernel) -> None: async with async_client.browsers.process.with_streaming_response.status( @@ -715,7 +715,7 @@ async def test_streaming_response_status(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_status(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -730,7 +730,7 @@ async def test_path_params_status(self, async_client: AsyncKernel) -> None: id="id", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_stdin(self, async_client: AsyncKernel) -> None: process = await async_client.browsers.process.stdin( @@ -740,7 +740,7 @@ async def test_method_stdin(self, async_client: AsyncKernel) -> None: ) assert_matches_type(ProcessStdinResponse, process, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_stdin(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.process.with_raw_response.stdin( @@ -754,7 +754,7 @@ async def test_raw_response_stdin(self, async_client: AsyncKernel) -> None: process = await response.parse() assert_matches_type(ProcessStdinResponse, process, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_stdin(self, async_client: AsyncKernel) -> None: async with async_client.browsers.process.with_streaming_response.stdin( @@ -770,7 +770,7 @@ async def test_streaming_response_stdin(self, async_client: AsyncKernel) -> None assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_stdin(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -787,7 +787,7 @@ async def test_path_params_stdin(self, async_client: AsyncKernel) -> None: data_b64="data_b64", ) - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize async def test_method_stdout_stream(self, async_client: AsyncKernel) -> None: process_stream = await async_client.browsers.process.stdout_stream( @@ -796,7 +796,7 @@ async def test_method_stdout_stream(self, async_client: AsyncKernel) -> None: ) await process_stream.response.aclose() - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize async def test_raw_response_stdout_stream(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.process.with_raw_response.stdout_stream( @@ -808,7 +808,7 @@ async def test_raw_response_stdout_stream(self, async_client: AsyncKernel) -> No stream = await response.parse() await stream.close() - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize async def test_streaming_response_stdout_stream(self, async_client: AsyncKernel) -> None: async with async_client.browsers.process.with_streaming_response.stdout_stream( @@ -823,7 +823,7 @@ async def test_streaming_response_stdout_stream(self, async_client: AsyncKernel) assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize async def test_path_params_stdout_stream(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): diff --git a/tests/api_resources/browsers/test_replays.py b/tests/api_resources/browsers/test_replays.py index df1fed56..72e986c1 100644 --- a/tests/api_resources/browsers/test_replays.py +++ b/tests/api_resources/browsers/test_replays.py @@ -25,7 +25,7 @@ class TestReplays: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_list(self, client: Kernel) -> None: replay = client.browsers.replays.list( @@ -33,7 +33,7 @@ def test_method_list(self, client: Kernel) -> None: ) assert_matches_type(ReplayListResponse, replay, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_list(self, client: Kernel) -> None: response = client.browsers.replays.with_raw_response.list( @@ -45,7 +45,7 @@ def test_raw_response_list(self, client: Kernel) -> None: replay = response.parse() assert_matches_type(ReplayListResponse, replay, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_list(self, client: Kernel) -> None: with client.browsers.replays.with_streaming_response.list( @@ -59,7 +59,7 @@ def test_streaming_response_list(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_list(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -127,7 +127,7 @@ def test_path_params_download(self, client: Kernel) -> None: id="id", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_start(self, client: Kernel) -> None: replay = client.browsers.replays.start( @@ -135,7 +135,7 @@ def test_method_start(self, client: Kernel) -> None: ) assert_matches_type(ReplayStartResponse, replay, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_start_with_all_params(self, client: Kernel) -> None: replay = client.browsers.replays.start( @@ -145,7 +145,7 @@ def test_method_start_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(ReplayStartResponse, replay, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_start(self, client: Kernel) -> None: response = client.browsers.replays.with_raw_response.start( @@ -157,7 +157,7 @@ def test_raw_response_start(self, client: Kernel) -> None: replay = response.parse() assert_matches_type(ReplayStartResponse, replay, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_start(self, client: Kernel) -> None: with client.browsers.replays.with_streaming_response.start( @@ -171,7 +171,7 @@ def test_streaming_response_start(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_start(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -179,7 +179,7 @@ def test_path_params_start(self, client: Kernel) -> None: id="", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_stop(self, client: Kernel) -> None: replay = client.browsers.replays.stop( @@ -188,7 +188,7 @@ def test_method_stop(self, client: Kernel) -> None: ) assert replay is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_stop(self, client: Kernel) -> None: response = client.browsers.replays.with_raw_response.stop( @@ -201,7 +201,7 @@ def test_raw_response_stop(self, client: Kernel) -> None: replay = response.parse() assert replay is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_stop(self, client: Kernel) -> None: with client.browsers.replays.with_streaming_response.stop( @@ -216,7 +216,7 @@ def test_streaming_response_stop(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_stop(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -237,7 +237,7 @@ class TestAsyncReplays: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_list(self, async_client: AsyncKernel) -> None: replay = await async_client.browsers.replays.list( @@ -245,7 +245,7 @@ async def test_method_list(self, async_client: AsyncKernel) -> None: ) assert_matches_type(ReplayListResponse, replay, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_list(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.replays.with_raw_response.list( @@ -257,7 +257,7 @@ async def test_raw_response_list(self, async_client: AsyncKernel) -> None: replay = await response.parse() assert_matches_type(ReplayListResponse, replay, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: async with async_client.browsers.replays.with_streaming_response.list( @@ -271,7 +271,7 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_list(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -339,7 +339,7 @@ async def test_path_params_download(self, async_client: AsyncKernel) -> None: id="id", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_start(self, async_client: AsyncKernel) -> None: replay = await async_client.browsers.replays.start( @@ -347,7 +347,7 @@ async def test_method_start(self, async_client: AsyncKernel) -> None: ) assert_matches_type(ReplayStartResponse, replay, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_start_with_all_params(self, async_client: AsyncKernel) -> None: replay = await async_client.browsers.replays.start( @@ -357,7 +357,7 @@ async def test_method_start_with_all_params(self, async_client: AsyncKernel) -> ) assert_matches_type(ReplayStartResponse, replay, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_start(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.replays.with_raw_response.start( @@ -369,7 +369,7 @@ async def test_raw_response_start(self, async_client: AsyncKernel) -> None: replay = await response.parse() assert_matches_type(ReplayStartResponse, replay, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_start(self, async_client: AsyncKernel) -> None: async with async_client.browsers.replays.with_streaming_response.start( @@ -383,7 +383,7 @@ async def test_streaming_response_start(self, async_client: AsyncKernel) -> None assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_start(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -391,7 +391,7 @@ async def test_path_params_start(self, async_client: AsyncKernel) -> None: id="", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_stop(self, async_client: AsyncKernel) -> None: replay = await async_client.browsers.replays.stop( @@ -400,7 +400,7 @@ async def test_method_stop(self, async_client: AsyncKernel) -> None: ) assert replay is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_stop(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.replays.with_raw_response.stop( @@ -413,7 +413,7 @@ async def test_raw_response_stop(self, async_client: AsyncKernel) -> None: replay = await response.parse() assert replay is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_stop(self, async_client: AsyncKernel) -> None: async with async_client.browsers.replays.with_streaming_response.stop( @@ -428,7 +428,7 @@ async def test_streaming_response_stop(self, async_client: AsyncKernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_stop(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): diff --git a/tests/api_resources/test_apps.py b/tests/api_resources/test_apps.py index 7475bcd5..c8629fa0 100644 --- a/tests/api_resources/test_apps.py +++ b/tests/api_resources/test_apps.py @@ -18,13 +18,13 @@ class TestApps: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_list(self, client: Kernel) -> None: app = client.apps.list() assert_matches_type(SyncOffsetPagination[AppListResponse], app, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_list_with_all_params(self, client: Kernel) -> None: app = client.apps.list( @@ -35,7 +35,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(SyncOffsetPagination[AppListResponse], app, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_list(self, client: Kernel) -> None: response = client.apps.with_raw_response.list() @@ -45,7 +45,7 @@ def test_raw_response_list(self, client: Kernel) -> None: app = response.parse() assert_matches_type(SyncOffsetPagination[AppListResponse], app, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_list(self, client: Kernel) -> None: with client.apps.with_streaming_response.list() as response: @@ -63,13 +63,13 @@ class TestAsyncApps: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_list(self, async_client: AsyncKernel) -> None: app = await async_client.apps.list() assert_matches_type(AsyncOffsetPagination[AppListResponse], app, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: app = await async_client.apps.list( @@ -80,7 +80,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N ) assert_matches_type(AsyncOffsetPagination[AppListResponse], app, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_list(self, async_client: AsyncKernel) -> None: response = await async_client.apps.with_raw_response.list() @@ -90,7 +90,7 @@ async def test_raw_response_list(self, async_client: AsyncKernel) -> None: app = await response.parse() assert_matches_type(AsyncOffsetPagination[AppListResponse], app, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: async with async_client.apps.with_streaming_response.list() as response: diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py index 6a8f164e..c7e1a477 100644 --- a/tests/api_resources/test_browser_pools.py +++ b/tests/api_resources/test_browser_pools.py @@ -21,7 +21,7 @@ class TestBrowserPools: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create(self, client: Kernel) -> None: browser_pool = client.browser_pools.create( @@ -29,7 +29,7 @@ def test_method_create(self, client: Kernel) -> None: ) assert_matches_type(BrowserPool, browser_pool, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create_with_all_params(self, client: Kernel) -> None: browser_pool = client.browser_pools.create( @@ -60,7 +60,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(BrowserPool, browser_pool, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_create(self, client: Kernel) -> None: response = client.browser_pools.with_raw_response.create( @@ -72,7 +72,7 @@ def test_raw_response_create(self, client: Kernel) -> None: browser_pool = response.parse() assert_matches_type(BrowserPool, browser_pool, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_create(self, client: Kernel) -> None: with client.browser_pools.with_streaming_response.create( @@ -86,7 +86,7 @@ def test_streaming_response_create(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_retrieve(self, client: Kernel) -> None: browser_pool = client.browser_pools.retrieve( @@ -94,7 +94,7 @@ def test_method_retrieve(self, client: Kernel) -> None: ) assert_matches_type(BrowserPool, browser_pool, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_retrieve(self, client: Kernel) -> None: response = client.browser_pools.with_raw_response.retrieve( @@ -106,7 +106,7 @@ def test_raw_response_retrieve(self, client: Kernel) -> None: browser_pool = response.parse() assert_matches_type(BrowserPool, browser_pool, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_retrieve(self, client: Kernel) -> None: with client.browser_pools.with_streaming_response.retrieve( @@ -120,7 +120,7 @@ def test_streaming_response_retrieve(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_retrieve(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): @@ -128,7 +128,7 @@ def test_path_params_retrieve(self, client: Kernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_update(self, client: Kernel) -> None: browser_pool = client.browser_pools.update( @@ -137,7 +137,7 @@ def test_method_update(self, client: Kernel) -> None: ) assert_matches_type(BrowserPool, browser_pool, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_update_with_all_params(self, client: Kernel) -> None: browser_pool = client.browser_pools.update( @@ -170,7 +170,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(BrowserPool, browser_pool, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_update(self, client: Kernel) -> None: response = client.browser_pools.with_raw_response.update( @@ -183,7 +183,7 @@ def test_raw_response_update(self, client: Kernel) -> None: browser_pool = response.parse() assert_matches_type(BrowserPool, browser_pool, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_update(self, client: Kernel) -> None: with client.browser_pools.with_streaming_response.update( @@ -198,7 +198,7 @@ def test_streaming_response_update(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_update(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): @@ -207,13 +207,13 @@ def test_path_params_update(self, client: Kernel) -> None: size=10, ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_list(self, client: Kernel) -> None: browser_pool = client.browser_pools.list() assert_matches_type(BrowserPoolListResponse, browser_pool, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_list(self, client: Kernel) -> None: response = client.browser_pools.with_raw_response.list() @@ -223,7 +223,7 @@ def test_raw_response_list(self, client: Kernel) -> None: browser_pool = response.parse() assert_matches_type(BrowserPoolListResponse, browser_pool, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_list(self, client: Kernel) -> None: with client.browser_pools.with_streaming_response.list() as response: @@ -235,7 +235,7 @@ def test_streaming_response_list(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_delete(self, client: Kernel) -> None: browser_pool = client.browser_pools.delete( @@ -243,7 +243,7 @@ def test_method_delete(self, client: Kernel) -> None: ) assert browser_pool is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_delete_with_all_params(self, client: Kernel) -> None: browser_pool = client.browser_pools.delete( @@ -252,7 +252,7 @@ def test_method_delete_with_all_params(self, client: Kernel) -> None: ) assert browser_pool is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_delete(self, client: Kernel) -> None: response = client.browser_pools.with_raw_response.delete( @@ -264,7 +264,7 @@ def test_raw_response_delete(self, client: Kernel) -> None: browser_pool = response.parse() assert browser_pool is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_delete(self, client: Kernel) -> None: with client.browser_pools.with_streaming_response.delete( @@ -278,7 +278,7 @@ def test_streaming_response_delete(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_delete(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): @@ -286,7 +286,7 @@ def test_path_params_delete(self, client: Kernel) -> None: id_or_name="", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_acquire(self, client: Kernel) -> None: browser_pool = client.browser_pools.acquire( @@ -294,7 +294,7 @@ def test_method_acquire(self, client: Kernel) -> None: ) assert_matches_type(BrowserPoolAcquireResponse, browser_pool, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_acquire_with_all_params(self, client: Kernel) -> None: browser_pool = client.browser_pools.acquire( @@ -303,7 +303,7 @@ def test_method_acquire_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(BrowserPoolAcquireResponse, browser_pool, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_acquire(self, client: Kernel) -> None: response = client.browser_pools.with_raw_response.acquire( @@ -315,7 +315,7 @@ def test_raw_response_acquire(self, client: Kernel) -> None: browser_pool = response.parse() assert_matches_type(BrowserPoolAcquireResponse, browser_pool, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_acquire(self, client: Kernel) -> None: with client.browser_pools.with_streaming_response.acquire( @@ -329,7 +329,7 @@ def test_streaming_response_acquire(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_acquire(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): @@ -337,7 +337,7 @@ def test_path_params_acquire(self, client: Kernel) -> None: id_or_name="", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_flush(self, client: Kernel) -> None: browser_pool = client.browser_pools.flush( @@ -345,7 +345,7 @@ def test_method_flush(self, client: Kernel) -> None: ) assert browser_pool is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_flush(self, client: Kernel) -> None: response = client.browser_pools.with_raw_response.flush( @@ -357,7 +357,7 @@ def test_raw_response_flush(self, client: Kernel) -> None: browser_pool = response.parse() assert browser_pool is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_flush(self, client: Kernel) -> None: with client.browser_pools.with_streaming_response.flush( @@ -371,7 +371,7 @@ def test_streaming_response_flush(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_flush(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): @@ -379,7 +379,7 @@ def test_path_params_flush(self, client: Kernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_release(self, client: Kernel) -> None: browser_pool = client.browser_pools.release( @@ -388,7 +388,7 @@ def test_method_release(self, client: Kernel) -> None: ) assert browser_pool is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_release_with_all_params(self, client: Kernel) -> None: browser_pool = client.browser_pools.release( @@ -398,7 +398,7 @@ def test_method_release_with_all_params(self, client: Kernel) -> None: ) assert browser_pool is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_release(self, client: Kernel) -> None: response = client.browser_pools.with_raw_response.release( @@ -411,7 +411,7 @@ def test_raw_response_release(self, client: Kernel) -> None: browser_pool = response.parse() assert browser_pool is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_release(self, client: Kernel) -> None: with client.browser_pools.with_streaming_response.release( @@ -426,7 +426,7 @@ def test_streaming_response_release(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_release(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): @@ -441,7 +441,7 @@ class TestAsyncBrowserPools: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create(self, async_client: AsyncKernel) -> None: browser_pool = await async_client.browser_pools.create( @@ -449,7 +449,7 @@ async def test_method_create(self, async_client: AsyncKernel) -> None: ) assert_matches_type(BrowserPool, browser_pool, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: browser_pool = await async_client.browser_pools.create( @@ -480,7 +480,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> ) assert_matches_type(BrowserPool, browser_pool, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_create(self, async_client: AsyncKernel) -> None: response = await async_client.browser_pools.with_raw_response.create( @@ -492,7 +492,7 @@ async def test_raw_response_create(self, async_client: AsyncKernel) -> None: browser_pool = await response.parse() assert_matches_type(BrowserPool, browser_pool, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: async with async_client.browser_pools.with_streaming_response.create( @@ -506,7 +506,7 @@ async def test_streaming_response_create(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_retrieve(self, async_client: AsyncKernel) -> None: browser_pool = await async_client.browser_pools.retrieve( @@ -514,7 +514,7 @@ async def test_method_retrieve(self, async_client: AsyncKernel) -> None: ) assert_matches_type(BrowserPool, browser_pool, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: response = await async_client.browser_pools.with_raw_response.retrieve( @@ -526,7 +526,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: browser_pool = await response.parse() assert_matches_type(BrowserPool, browser_pool, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: async with async_client.browser_pools.with_streaming_response.retrieve( @@ -540,7 +540,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> N assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): @@ -548,7 +548,7 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_update(self, async_client: AsyncKernel) -> None: browser_pool = await async_client.browser_pools.update( @@ -557,7 +557,7 @@ async def test_method_update(self, async_client: AsyncKernel) -> None: ) assert_matches_type(BrowserPool, browser_pool, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> None: browser_pool = await async_client.browser_pools.update( @@ -590,7 +590,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> ) assert_matches_type(BrowserPool, browser_pool, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_update(self, async_client: AsyncKernel) -> None: response = await async_client.browser_pools.with_raw_response.update( @@ -603,7 +603,7 @@ async def test_raw_response_update(self, async_client: AsyncKernel) -> None: browser_pool = await response.parse() assert_matches_type(BrowserPool, browser_pool, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_update(self, async_client: AsyncKernel) -> None: async with async_client.browser_pools.with_streaming_response.update( @@ -618,7 +618,7 @@ async def test_streaming_response_update(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_update(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): @@ -627,13 +627,13 @@ async def test_path_params_update(self, async_client: AsyncKernel) -> None: size=10, ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_list(self, async_client: AsyncKernel) -> None: browser_pool = await async_client.browser_pools.list() assert_matches_type(BrowserPoolListResponse, browser_pool, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_list(self, async_client: AsyncKernel) -> None: response = await async_client.browser_pools.with_raw_response.list() @@ -643,7 +643,7 @@ async def test_raw_response_list(self, async_client: AsyncKernel) -> None: browser_pool = await response.parse() assert_matches_type(BrowserPoolListResponse, browser_pool, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: async with async_client.browser_pools.with_streaming_response.list() as response: @@ -655,7 +655,7 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_delete(self, async_client: AsyncKernel) -> None: browser_pool = await async_client.browser_pools.delete( @@ -663,7 +663,7 @@ async def test_method_delete(self, async_client: AsyncKernel) -> None: ) assert browser_pool is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_delete_with_all_params(self, async_client: AsyncKernel) -> None: browser_pool = await async_client.browser_pools.delete( @@ -672,7 +672,7 @@ async def test_method_delete_with_all_params(self, async_client: AsyncKernel) -> ) assert browser_pool is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: response = await async_client.browser_pools.with_raw_response.delete( @@ -684,7 +684,7 @@ async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: browser_pool = await response.parse() assert browser_pool is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None: async with async_client.browser_pools.with_streaming_response.delete( @@ -698,7 +698,7 @@ async def test_streaming_response_delete(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_delete(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): @@ -706,7 +706,7 @@ async def test_path_params_delete(self, async_client: AsyncKernel) -> None: id_or_name="", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_acquire(self, async_client: AsyncKernel) -> None: browser_pool = await async_client.browser_pools.acquire( @@ -714,7 +714,7 @@ async def test_method_acquire(self, async_client: AsyncKernel) -> None: ) assert_matches_type(BrowserPoolAcquireResponse, browser_pool, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_acquire_with_all_params(self, async_client: AsyncKernel) -> None: browser_pool = await async_client.browser_pools.acquire( @@ -723,7 +723,7 @@ async def test_method_acquire_with_all_params(self, async_client: AsyncKernel) - ) assert_matches_type(BrowserPoolAcquireResponse, browser_pool, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_acquire(self, async_client: AsyncKernel) -> None: response = await async_client.browser_pools.with_raw_response.acquire( @@ -735,7 +735,7 @@ async def test_raw_response_acquire(self, async_client: AsyncKernel) -> None: browser_pool = await response.parse() assert_matches_type(BrowserPoolAcquireResponse, browser_pool, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_acquire(self, async_client: AsyncKernel) -> None: async with async_client.browser_pools.with_streaming_response.acquire( @@ -749,7 +749,7 @@ async def test_streaming_response_acquire(self, async_client: AsyncKernel) -> No assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_acquire(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): @@ -757,7 +757,7 @@ async def test_path_params_acquire(self, async_client: AsyncKernel) -> None: id_or_name="", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_flush(self, async_client: AsyncKernel) -> None: browser_pool = await async_client.browser_pools.flush( @@ -765,7 +765,7 @@ async def test_method_flush(self, async_client: AsyncKernel) -> None: ) assert browser_pool is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_flush(self, async_client: AsyncKernel) -> None: response = await async_client.browser_pools.with_raw_response.flush( @@ -777,7 +777,7 @@ async def test_raw_response_flush(self, async_client: AsyncKernel) -> None: browser_pool = await response.parse() assert browser_pool is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_flush(self, async_client: AsyncKernel) -> None: async with async_client.browser_pools.with_streaming_response.flush( @@ -791,7 +791,7 @@ async def test_streaming_response_flush(self, async_client: AsyncKernel) -> None assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_flush(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): @@ -799,7 +799,7 @@ async def test_path_params_flush(self, async_client: AsyncKernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_release(self, async_client: AsyncKernel) -> None: browser_pool = await async_client.browser_pools.release( @@ -808,7 +808,7 @@ async def test_method_release(self, async_client: AsyncKernel) -> None: ) assert browser_pool is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_release_with_all_params(self, async_client: AsyncKernel) -> None: browser_pool = await async_client.browser_pools.release( @@ -818,7 +818,7 @@ async def test_method_release_with_all_params(self, async_client: AsyncKernel) - ) assert browser_pool is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_release(self, async_client: AsyncKernel) -> None: response = await async_client.browser_pools.with_raw_response.release( @@ -831,7 +831,7 @@ async def test_raw_response_release(self, async_client: AsyncKernel) -> None: browser_pool = await response.parse() assert browser_pool is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_release(self, async_client: AsyncKernel) -> None: async with async_client.browser_pools.with_streaming_response.release( @@ -846,7 +846,7 @@ async def test_streaming_response_release(self, async_client: AsyncKernel) -> No assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_release(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index fc2d4d09..c0319df9 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -25,13 +25,13 @@ class TestBrowsers: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create(self, client: Kernel) -> None: browser = client.browsers.create() assert_matches_type(BrowserCreateResponse, browser, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create_with_all_params(self, client: Kernel) -> None: browser = client.browsers.create( @@ -62,7 +62,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(BrowserCreateResponse, browser, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_create(self, client: Kernel) -> None: response = client.browsers.with_raw_response.create() @@ -72,7 +72,7 @@ def test_raw_response_create(self, client: Kernel) -> None: browser = response.parse() assert_matches_type(BrowserCreateResponse, browser, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_create(self, client: Kernel) -> None: with client.browsers.with_streaming_response.create() as response: @@ -84,7 +84,7 @@ def test_streaming_response_create(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_retrieve(self, client: Kernel) -> None: browser = client.browsers.retrieve( @@ -92,7 +92,7 @@ def test_method_retrieve(self, client: Kernel) -> None: ) assert_matches_type(BrowserRetrieveResponse, browser, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_retrieve_with_all_params(self, client: Kernel) -> None: browser = client.browsers.retrieve( @@ -101,7 +101,7 @@ def test_method_retrieve_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(BrowserRetrieveResponse, browser, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_retrieve(self, client: Kernel) -> None: response = client.browsers.with_raw_response.retrieve( @@ -113,7 +113,7 @@ def test_raw_response_retrieve(self, client: Kernel) -> None: browser = response.parse() assert_matches_type(BrowserRetrieveResponse, browser, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_retrieve(self, client: Kernel) -> None: with client.browsers.with_streaming_response.retrieve( @@ -127,7 +127,7 @@ def test_streaming_response_retrieve(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_retrieve(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -135,7 +135,7 @@ def test_path_params_retrieve(self, client: Kernel) -> None: id="", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_update(self, client: Kernel) -> None: browser = client.browsers.update( @@ -143,7 +143,7 @@ def test_method_update(self, client: Kernel) -> None: ) assert_matches_type(BrowserUpdateResponse, browser, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_update_with_all_params(self, client: Kernel) -> None: browser = client.browsers.update( @@ -162,7 +162,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(BrowserUpdateResponse, browser, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_update(self, client: Kernel) -> None: response = client.browsers.with_raw_response.update( @@ -174,7 +174,7 @@ def test_raw_response_update(self, client: Kernel) -> None: browser = response.parse() assert_matches_type(BrowserUpdateResponse, browser, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_update(self, client: Kernel) -> None: with client.browsers.with_streaming_response.update( @@ -188,7 +188,7 @@ def test_streaming_response_update(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_update(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -196,13 +196,13 @@ def test_path_params_update(self, client: Kernel) -> None: id="", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_list(self, client: Kernel) -> None: browser = client.browsers.list() assert_matches_type(SyncOffsetPagination[BrowserListResponse], browser, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_list_with_all_params(self, client: Kernel) -> None: browser = client.browsers.list( @@ -213,7 +213,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(SyncOffsetPagination[BrowserListResponse], browser, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_list(self, client: Kernel) -> None: response = client.browsers.with_raw_response.list() @@ -223,7 +223,7 @@ def test_raw_response_list(self, client: Kernel) -> None: browser = response.parse() assert_matches_type(SyncOffsetPagination[BrowserListResponse], browser, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_list(self, client: Kernel) -> None: with client.browsers.with_streaming_response.list() as response: @@ -235,7 +235,7 @@ def test_streaming_response_list(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_delete(self, client: Kernel) -> None: with pytest.warns(DeprecationWarning): @@ -245,7 +245,7 @@ def test_method_delete(self, client: Kernel) -> None: assert browser is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_delete(self, client: Kernel) -> None: with pytest.warns(DeprecationWarning): @@ -258,7 +258,7 @@ def test_raw_response_delete(self, client: Kernel) -> None: browser = response.parse() assert browser is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_delete(self, client: Kernel) -> None: with pytest.warns(DeprecationWarning): @@ -273,7 +273,7 @@ def test_streaming_response_delete(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_delete_by_id(self, client: Kernel) -> None: browser = client.browsers.delete_by_id( @@ -281,7 +281,7 @@ def test_method_delete_by_id(self, client: Kernel) -> None: ) assert browser is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_delete_by_id(self, client: Kernel) -> None: response = client.browsers.with_raw_response.delete_by_id( @@ -293,7 +293,7 @@ def test_raw_response_delete_by_id(self, client: Kernel) -> None: browser = response.parse() assert browser is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_delete_by_id(self, client: Kernel) -> None: with client.browsers.with_streaming_response.delete_by_id( @@ -307,7 +307,7 @@ def test_streaming_response_delete_by_id(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_delete_by_id(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -315,7 +315,7 @@ def test_path_params_delete_by_id(self, client: Kernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_load_extensions(self, client: Kernel) -> None: browser = client.browsers.load_extensions( @@ -329,7 +329,7 @@ def test_method_load_extensions(self, client: Kernel) -> None: ) assert browser is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_load_extensions(self, client: Kernel) -> None: response = client.browsers.with_raw_response.load_extensions( @@ -347,7 +347,7 @@ def test_raw_response_load_extensions(self, client: Kernel) -> None: browser = response.parse() assert browser is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_load_extensions(self, client: Kernel) -> None: with client.browsers.with_streaming_response.load_extensions( @@ -367,7 +367,7 @@ def test_streaming_response_load_extensions(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_load_extensions(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -387,13 +387,13 @@ class TestAsyncBrowsers: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create(self, async_client: AsyncKernel) -> None: browser = await async_client.browsers.create() assert_matches_type(BrowserCreateResponse, browser, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: browser = await async_client.browsers.create( @@ -424,7 +424,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> ) assert_matches_type(BrowserCreateResponse, browser, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_create(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.with_raw_response.create() @@ -434,7 +434,7 @@ async def test_raw_response_create(self, async_client: AsyncKernel) -> None: browser = await response.parse() assert_matches_type(BrowserCreateResponse, browser, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: async with async_client.browsers.with_streaming_response.create() as response: @@ -446,7 +446,7 @@ async def test_streaming_response_create(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_retrieve(self, async_client: AsyncKernel) -> None: browser = await async_client.browsers.retrieve( @@ -454,7 +454,7 @@ async def test_method_retrieve(self, async_client: AsyncKernel) -> None: ) assert_matches_type(BrowserRetrieveResponse, browser, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_retrieve_with_all_params(self, async_client: AsyncKernel) -> None: browser = await async_client.browsers.retrieve( @@ -463,7 +463,7 @@ async def test_method_retrieve_with_all_params(self, async_client: AsyncKernel) ) assert_matches_type(BrowserRetrieveResponse, browser, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.with_raw_response.retrieve( @@ -475,7 +475,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: browser = await response.parse() assert_matches_type(BrowserRetrieveResponse, browser, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: async with async_client.browsers.with_streaming_response.retrieve( @@ -489,7 +489,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> N assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -497,7 +497,7 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: id="", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_update(self, async_client: AsyncKernel) -> None: browser = await async_client.browsers.update( @@ -505,7 +505,7 @@ async def test_method_update(self, async_client: AsyncKernel) -> None: ) assert_matches_type(BrowserUpdateResponse, browser, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> None: browser = await async_client.browsers.update( @@ -524,7 +524,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> ) assert_matches_type(BrowserUpdateResponse, browser, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_update(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.with_raw_response.update( @@ -536,7 +536,7 @@ async def test_raw_response_update(self, async_client: AsyncKernel) -> None: browser = await response.parse() assert_matches_type(BrowserUpdateResponse, browser, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_update(self, async_client: AsyncKernel) -> None: async with async_client.browsers.with_streaming_response.update( @@ -550,7 +550,7 @@ async def test_streaming_response_update(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_update(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -558,13 +558,13 @@ async def test_path_params_update(self, async_client: AsyncKernel) -> None: id="", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_list(self, async_client: AsyncKernel) -> None: browser = await async_client.browsers.list() assert_matches_type(AsyncOffsetPagination[BrowserListResponse], browser, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: browser = await async_client.browsers.list( @@ -575,7 +575,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N ) assert_matches_type(AsyncOffsetPagination[BrowserListResponse], browser, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_list(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.with_raw_response.list() @@ -585,7 +585,7 @@ async def test_raw_response_list(self, async_client: AsyncKernel) -> None: browser = await response.parse() assert_matches_type(AsyncOffsetPagination[BrowserListResponse], browser, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: async with async_client.browsers.with_streaming_response.list() as response: @@ -597,7 +597,7 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_delete(self, async_client: AsyncKernel) -> None: with pytest.warns(DeprecationWarning): @@ -607,7 +607,7 @@ async def test_method_delete(self, async_client: AsyncKernel) -> None: assert browser is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: with pytest.warns(DeprecationWarning): @@ -620,7 +620,7 @@ async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: browser = await response.parse() assert browser is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None: with pytest.warns(DeprecationWarning): @@ -635,7 +635,7 @@ async def test_streaming_response_delete(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_delete_by_id(self, async_client: AsyncKernel) -> None: browser = await async_client.browsers.delete_by_id( @@ -643,7 +643,7 @@ async def test_method_delete_by_id(self, async_client: AsyncKernel) -> None: ) assert browser is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_delete_by_id(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.with_raw_response.delete_by_id( @@ -655,7 +655,7 @@ async def test_raw_response_delete_by_id(self, async_client: AsyncKernel) -> Non browser = await response.parse() assert browser is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_delete_by_id(self, async_client: AsyncKernel) -> None: async with async_client.browsers.with_streaming_response.delete_by_id( @@ -669,7 +669,7 @@ async def test_streaming_response_delete_by_id(self, async_client: AsyncKernel) assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_delete_by_id(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -677,7 +677,7 @@ async def test_path_params_delete_by_id(self, async_client: AsyncKernel) -> None "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_load_extensions(self, async_client: AsyncKernel) -> None: browser = await async_client.browsers.load_extensions( @@ -691,7 +691,7 @@ async def test_method_load_extensions(self, async_client: AsyncKernel) -> None: ) assert browser is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_load_extensions(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.with_raw_response.load_extensions( @@ -709,7 +709,7 @@ async def test_raw_response_load_extensions(self, async_client: AsyncKernel) -> browser = await response.parse() assert browser is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_load_extensions(self, async_client: AsyncKernel) -> None: async with async_client.browsers.with_streaming_response.load_extensions( @@ -729,7 +729,7 @@ async def test_streaming_response_load_extensions(self, async_client: AsyncKerne assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_load_extensions(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): diff --git a/tests/api_resources/test_credential_providers.py b/tests/api_resources/test_credential_providers.py index f110523f..93f44145 100644 --- a/tests/api_resources/test_credential_providers.py +++ b/tests/api_resources/test_credential_providers.py @@ -22,7 +22,7 @@ class TestCredentialProviders: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create(self, client: Kernel) -> None: credential_provider = client.credential_providers.create( @@ -32,7 +32,7 @@ def test_method_create(self, client: Kernel) -> None: ) assert_matches_type(CredentialProvider, credential_provider, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create_with_all_params(self, client: Kernel) -> None: credential_provider = client.credential_providers.create( @@ -43,7 +43,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(CredentialProvider, credential_provider, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_create(self, client: Kernel) -> None: response = client.credential_providers.with_raw_response.create( @@ -57,7 +57,7 @@ def test_raw_response_create(self, client: Kernel) -> None: credential_provider = response.parse() assert_matches_type(CredentialProvider, credential_provider, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_create(self, client: Kernel) -> None: with client.credential_providers.with_streaming_response.create( @@ -73,7 +73,7 @@ def test_streaming_response_create(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_retrieve(self, client: Kernel) -> None: credential_provider = client.credential_providers.retrieve( @@ -81,7 +81,7 @@ def test_method_retrieve(self, client: Kernel) -> None: ) assert_matches_type(CredentialProvider, credential_provider, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_retrieve(self, client: Kernel) -> None: response = client.credential_providers.with_raw_response.retrieve( @@ -93,7 +93,7 @@ def test_raw_response_retrieve(self, client: Kernel) -> None: credential_provider = response.parse() assert_matches_type(CredentialProvider, credential_provider, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_retrieve(self, client: Kernel) -> None: with client.credential_providers.with_streaming_response.retrieve( @@ -107,7 +107,7 @@ def test_streaming_response_retrieve(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_retrieve(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -115,7 +115,7 @@ def test_path_params_retrieve(self, client: Kernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_update(self, client: Kernel) -> None: credential_provider = client.credential_providers.update( @@ -123,7 +123,7 @@ def test_method_update(self, client: Kernel) -> None: ) assert_matches_type(CredentialProvider, credential_provider, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_update_with_all_params(self, client: Kernel) -> None: credential_provider = client.credential_providers.update( @@ -136,7 +136,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(CredentialProvider, credential_provider, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_update(self, client: Kernel) -> None: response = client.credential_providers.with_raw_response.update( @@ -148,7 +148,7 @@ def test_raw_response_update(self, client: Kernel) -> None: credential_provider = response.parse() assert_matches_type(CredentialProvider, credential_provider, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_update(self, client: Kernel) -> None: with client.credential_providers.with_streaming_response.update( @@ -162,7 +162,7 @@ def test_streaming_response_update(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_update(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -170,13 +170,13 @@ def test_path_params_update(self, client: Kernel) -> None: id="", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_list(self, client: Kernel) -> None: credential_provider = client.credential_providers.list() assert_matches_type(CredentialProviderListResponse, credential_provider, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_list(self, client: Kernel) -> None: response = client.credential_providers.with_raw_response.list() @@ -186,7 +186,7 @@ def test_raw_response_list(self, client: Kernel) -> None: credential_provider = response.parse() assert_matches_type(CredentialProviderListResponse, credential_provider, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_list(self, client: Kernel) -> None: with client.credential_providers.with_streaming_response.list() as response: @@ -198,7 +198,7 @@ def test_streaming_response_list(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_delete(self, client: Kernel) -> None: credential_provider = client.credential_providers.delete( @@ -206,7 +206,7 @@ def test_method_delete(self, client: Kernel) -> None: ) assert credential_provider is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_delete(self, client: Kernel) -> None: response = client.credential_providers.with_raw_response.delete( @@ -218,7 +218,7 @@ def test_raw_response_delete(self, client: Kernel) -> None: credential_provider = response.parse() assert credential_provider is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_delete(self, client: Kernel) -> None: with client.credential_providers.with_streaming_response.delete( @@ -232,7 +232,7 @@ def test_streaming_response_delete(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_delete(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -240,7 +240,7 @@ def test_path_params_delete(self, client: Kernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_list_items(self, client: Kernel) -> None: credential_provider = client.credential_providers.list_items( @@ -248,7 +248,7 @@ def test_method_list_items(self, client: Kernel) -> None: ) assert_matches_type(CredentialProviderListItemsResponse, credential_provider, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_list_items(self, client: Kernel) -> None: response = client.credential_providers.with_raw_response.list_items( @@ -260,7 +260,7 @@ def test_raw_response_list_items(self, client: Kernel) -> None: credential_provider = response.parse() assert_matches_type(CredentialProviderListItemsResponse, credential_provider, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_list_items(self, client: Kernel) -> None: with client.credential_providers.with_streaming_response.list_items( @@ -274,7 +274,7 @@ def test_streaming_response_list_items(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_list_items(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -282,7 +282,7 @@ def test_path_params_list_items(self, client: Kernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_test(self, client: Kernel) -> None: credential_provider = client.credential_providers.test( @@ -290,7 +290,7 @@ def test_method_test(self, client: Kernel) -> None: ) assert_matches_type(CredentialProviderTestResult, credential_provider, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_test(self, client: Kernel) -> None: response = client.credential_providers.with_raw_response.test( @@ -302,7 +302,7 @@ def test_raw_response_test(self, client: Kernel) -> None: credential_provider = response.parse() assert_matches_type(CredentialProviderTestResult, credential_provider, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_test(self, client: Kernel) -> None: with client.credential_providers.with_streaming_response.test( @@ -316,7 +316,7 @@ def test_streaming_response_test(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_test(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -330,7 +330,7 @@ class TestAsyncCredentialProviders: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create(self, async_client: AsyncKernel) -> None: credential_provider = await async_client.credential_providers.create( @@ -340,7 +340,7 @@ async def test_method_create(self, async_client: AsyncKernel) -> None: ) assert_matches_type(CredentialProvider, credential_provider, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: credential_provider = await async_client.credential_providers.create( @@ -351,7 +351,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> ) assert_matches_type(CredentialProvider, credential_provider, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_create(self, async_client: AsyncKernel) -> None: response = await async_client.credential_providers.with_raw_response.create( @@ -365,7 +365,7 @@ async def test_raw_response_create(self, async_client: AsyncKernel) -> None: credential_provider = await response.parse() assert_matches_type(CredentialProvider, credential_provider, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: async with async_client.credential_providers.with_streaming_response.create( @@ -381,7 +381,7 @@ async def test_streaming_response_create(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_retrieve(self, async_client: AsyncKernel) -> None: credential_provider = await async_client.credential_providers.retrieve( @@ -389,7 +389,7 @@ async def test_method_retrieve(self, async_client: AsyncKernel) -> None: ) assert_matches_type(CredentialProvider, credential_provider, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: response = await async_client.credential_providers.with_raw_response.retrieve( @@ -401,7 +401,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: credential_provider = await response.parse() assert_matches_type(CredentialProvider, credential_provider, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: async with async_client.credential_providers.with_streaming_response.retrieve( @@ -415,7 +415,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> N assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -423,7 +423,7 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_update(self, async_client: AsyncKernel) -> None: credential_provider = await async_client.credential_providers.update( @@ -431,7 +431,7 @@ async def test_method_update(self, async_client: AsyncKernel) -> None: ) assert_matches_type(CredentialProvider, credential_provider, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> None: credential_provider = await async_client.credential_providers.update( @@ -444,7 +444,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> ) assert_matches_type(CredentialProvider, credential_provider, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_update(self, async_client: AsyncKernel) -> None: response = await async_client.credential_providers.with_raw_response.update( @@ -456,7 +456,7 @@ async def test_raw_response_update(self, async_client: AsyncKernel) -> None: credential_provider = await response.parse() assert_matches_type(CredentialProvider, credential_provider, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_update(self, async_client: AsyncKernel) -> None: async with async_client.credential_providers.with_streaming_response.update( @@ -470,7 +470,7 @@ async def test_streaming_response_update(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_update(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -478,13 +478,13 @@ async def test_path_params_update(self, async_client: AsyncKernel) -> None: id="", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_list(self, async_client: AsyncKernel) -> None: credential_provider = await async_client.credential_providers.list() assert_matches_type(CredentialProviderListResponse, credential_provider, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_list(self, async_client: AsyncKernel) -> None: response = await async_client.credential_providers.with_raw_response.list() @@ -494,7 +494,7 @@ async def test_raw_response_list(self, async_client: AsyncKernel) -> None: credential_provider = await response.parse() assert_matches_type(CredentialProviderListResponse, credential_provider, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: async with async_client.credential_providers.with_streaming_response.list() as response: @@ -506,7 +506,7 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_delete(self, async_client: AsyncKernel) -> None: credential_provider = await async_client.credential_providers.delete( @@ -514,7 +514,7 @@ async def test_method_delete(self, async_client: AsyncKernel) -> None: ) assert credential_provider is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: response = await async_client.credential_providers.with_raw_response.delete( @@ -526,7 +526,7 @@ async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: credential_provider = await response.parse() assert credential_provider is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None: async with async_client.credential_providers.with_streaming_response.delete( @@ -540,7 +540,7 @@ async def test_streaming_response_delete(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_delete(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -548,7 +548,7 @@ async def test_path_params_delete(self, async_client: AsyncKernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_list_items(self, async_client: AsyncKernel) -> None: credential_provider = await async_client.credential_providers.list_items( @@ -556,7 +556,7 @@ async def test_method_list_items(self, async_client: AsyncKernel) -> None: ) assert_matches_type(CredentialProviderListItemsResponse, credential_provider, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_list_items(self, async_client: AsyncKernel) -> None: response = await async_client.credential_providers.with_raw_response.list_items( @@ -568,7 +568,7 @@ async def test_raw_response_list_items(self, async_client: AsyncKernel) -> None: credential_provider = await response.parse() assert_matches_type(CredentialProviderListItemsResponse, credential_provider, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_list_items(self, async_client: AsyncKernel) -> None: async with async_client.credential_providers.with_streaming_response.list_items( @@ -582,7 +582,7 @@ async def test_streaming_response_list_items(self, async_client: AsyncKernel) -> assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_list_items(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -590,7 +590,7 @@ async def test_path_params_list_items(self, async_client: AsyncKernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_test(self, async_client: AsyncKernel) -> None: credential_provider = await async_client.credential_providers.test( @@ -598,7 +598,7 @@ async def test_method_test(self, async_client: AsyncKernel) -> None: ) assert_matches_type(CredentialProviderTestResult, credential_provider, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_test(self, async_client: AsyncKernel) -> None: response = await async_client.credential_providers.with_raw_response.test( @@ -610,7 +610,7 @@ async def test_raw_response_test(self, async_client: AsyncKernel) -> None: credential_provider = await response.parse() assert_matches_type(CredentialProviderTestResult, credential_provider, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_test(self, async_client: AsyncKernel) -> None: async with async_client.credential_providers.with_streaming_response.test( @@ -624,7 +624,7 @@ async def test_streaming_response_test(self, async_client: AsyncKernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_test(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): diff --git a/tests/api_resources/test_credentials.py b/tests/api_resources/test_credentials.py index b6098685..ff932bef 100644 --- a/tests/api_resources/test_credentials.py +++ b/tests/api_resources/test_credentials.py @@ -21,7 +21,7 @@ class TestCredentials: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create(self, client: Kernel) -> None: credential = client.credentials.create( @@ -34,7 +34,7 @@ def test_method_create(self, client: Kernel) -> None: ) assert_matches_type(Credential, credential, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create_with_all_params(self, client: Kernel) -> None: credential = client.credentials.create( @@ -49,7 +49,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(Credential, credential, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_create(self, client: Kernel) -> None: response = client.credentials.with_raw_response.create( @@ -66,7 +66,7 @@ def test_raw_response_create(self, client: Kernel) -> None: credential = response.parse() assert_matches_type(Credential, credential, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_create(self, client: Kernel) -> None: with client.credentials.with_streaming_response.create( @@ -85,7 +85,7 @@ def test_streaming_response_create(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_retrieve(self, client: Kernel) -> None: credential = client.credentials.retrieve( @@ -93,7 +93,7 @@ def test_method_retrieve(self, client: Kernel) -> None: ) assert_matches_type(Credential, credential, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_retrieve(self, client: Kernel) -> None: response = client.credentials.with_raw_response.retrieve( @@ -105,7 +105,7 @@ def test_raw_response_retrieve(self, client: Kernel) -> None: credential = response.parse() assert_matches_type(Credential, credential, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_retrieve(self, client: Kernel) -> None: with client.credentials.with_streaming_response.retrieve( @@ -119,7 +119,7 @@ def test_streaming_response_retrieve(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_retrieve(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): @@ -127,7 +127,7 @@ def test_path_params_retrieve(self, client: Kernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_update(self, client: Kernel) -> None: credential = client.credentials.update( @@ -135,7 +135,7 @@ def test_method_update(self, client: Kernel) -> None: ) assert_matches_type(Credential, credential, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_update_with_all_params(self, client: Kernel) -> None: credential = client.credentials.update( @@ -150,7 +150,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(Credential, credential, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_update(self, client: Kernel) -> None: response = client.credentials.with_raw_response.update( @@ -162,7 +162,7 @@ def test_raw_response_update(self, client: Kernel) -> None: credential = response.parse() assert_matches_type(Credential, credential, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_update(self, client: Kernel) -> None: with client.credentials.with_streaming_response.update( @@ -176,7 +176,7 @@ def test_streaming_response_update(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_update(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): @@ -184,13 +184,13 @@ def test_path_params_update(self, client: Kernel) -> None: id_or_name="", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_list(self, client: Kernel) -> None: credential = client.credentials.list() assert_matches_type(SyncOffsetPagination[Credential], credential, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_list_with_all_params(self, client: Kernel) -> None: credential = client.credentials.list( @@ -200,7 +200,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(SyncOffsetPagination[Credential], credential, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_list(self, client: Kernel) -> None: response = client.credentials.with_raw_response.list() @@ -210,7 +210,7 @@ def test_raw_response_list(self, client: Kernel) -> None: credential = response.parse() assert_matches_type(SyncOffsetPagination[Credential], credential, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_list(self, client: Kernel) -> None: with client.credentials.with_streaming_response.list() as response: @@ -222,7 +222,7 @@ def test_streaming_response_list(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_delete(self, client: Kernel) -> None: credential = client.credentials.delete( @@ -230,7 +230,7 @@ def test_method_delete(self, client: Kernel) -> None: ) assert credential is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_delete(self, client: Kernel) -> None: response = client.credentials.with_raw_response.delete( @@ -242,7 +242,7 @@ def test_raw_response_delete(self, client: Kernel) -> None: credential = response.parse() assert credential is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_delete(self, client: Kernel) -> None: with client.credentials.with_streaming_response.delete( @@ -256,7 +256,7 @@ def test_streaming_response_delete(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_delete(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): @@ -264,7 +264,7 @@ def test_path_params_delete(self, client: Kernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_totp_code(self, client: Kernel) -> None: credential = client.credentials.totp_code( @@ -272,7 +272,7 @@ def test_method_totp_code(self, client: Kernel) -> None: ) assert_matches_type(CredentialTotpCodeResponse, credential, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_totp_code(self, client: Kernel) -> None: response = client.credentials.with_raw_response.totp_code( @@ -284,7 +284,7 @@ def test_raw_response_totp_code(self, client: Kernel) -> None: credential = response.parse() assert_matches_type(CredentialTotpCodeResponse, credential, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_totp_code(self, client: Kernel) -> None: with client.credentials.with_streaming_response.totp_code( @@ -298,7 +298,7 @@ def test_streaming_response_totp_code(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_totp_code(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): @@ -312,7 +312,7 @@ class TestAsyncCredentials: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create(self, async_client: AsyncKernel) -> None: credential = await async_client.credentials.create( @@ -325,7 +325,7 @@ async def test_method_create(self, async_client: AsyncKernel) -> None: ) assert_matches_type(Credential, credential, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: credential = await async_client.credentials.create( @@ -340,7 +340,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> ) assert_matches_type(Credential, credential, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_create(self, async_client: AsyncKernel) -> None: response = await async_client.credentials.with_raw_response.create( @@ -357,7 +357,7 @@ async def test_raw_response_create(self, async_client: AsyncKernel) -> None: credential = await response.parse() assert_matches_type(Credential, credential, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: async with async_client.credentials.with_streaming_response.create( @@ -376,7 +376,7 @@ async def test_streaming_response_create(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_retrieve(self, async_client: AsyncKernel) -> None: credential = await async_client.credentials.retrieve( @@ -384,7 +384,7 @@ async def test_method_retrieve(self, async_client: AsyncKernel) -> None: ) assert_matches_type(Credential, credential, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: response = await async_client.credentials.with_raw_response.retrieve( @@ -396,7 +396,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: credential = await response.parse() assert_matches_type(Credential, credential, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: async with async_client.credentials.with_streaming_response.retrieve( @@ -410,7 +410,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> N assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): @@ -418,7 +418,7 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_update(self, async_client: AsyncKernel) -> None: credential = await async_client.credentials.update( @@ -426,7 +426,7 @@ async def test_method_update(self, async_client: AsyncKernel) -> None: ) assert_matches_type(Credential, credential, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> None: credential = await async_client.credentials.update( @@ -441,7 +441,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> ) assert_matches_type(Credential, credential, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_update(self, async_client: AsyncKernel) -> None: response = await async_client.credentials.with_raw_response.update( @@ -453,7 +453,7 @@ async def test_raw_response_update(self, async_client: AsyncKernel) -> None: credential = await response.parse() assert_matches_type(Credential, credential, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_update(self, async_client: AsyncKernel) -> None: async with async_client.credentials.with_streaming_response.update( @@ -467,7 +467,7 @@ async def test_streaming_response_update(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_update(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): @@ -475,13 +475,13 @@ async def test_path_params_update(self, async_client: AsyncKernel) -> None: id_or_name="", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_list(self, async_client: AsyncKernel) -> None: credential = await async_client.credentials.list() assert_matches_type(AsyncOffsetPagination[Credential], credential, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: credential = await async_client.credentials.list( @@ -491,7 +491,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N ) assert_matches_type(AsyncOffsetPagination[Credential], credential, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_list(self, async_client: AsyncKernel) -> None: response = await async_client.credentials.with_raw_response.list() @@ -501,7 +501,7 @@ async def test_raw_response_list(self, async_client: AsyncKernel) -> None: credential = await response.parse() assert_matches_type(AsyncOffsetPagination[Credential], credential, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: async with async_client.credentials.with_streaming_response.list() as response: @@ -513,7 +513,7 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_delete(self, async_client: AsyncKernel) -> None: credential = await async_client.credentials.delete( @@ -521,7 +521,7 @@ async def test_method_delete(self, async_client: AsyncKernel) -> None: ) assert credential is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: response = await async_client.credentials.with_raw_response.delete( @@ -533,7 +533,7 @@ async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: credential = await response.parse() assert credential is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None: async with async_client.credentials.with_streaming_response.delete( @@ -547,7 +547,7 @@ async def test_streaming_response_delete(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_delete(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): @@ -555,7 +555,7 @@ async def test_path_params_delete(self, async_client: AsyncKernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_totp_code(self, async_client: AsyncKernel) -> None: credential = await async_client.credentials.totp_code( @@ -563,7 +563,7 @@ async def test_method_totp_code(self, async_client: AsyncKernel) -> None: ) assert_matches_type(CredentialTotpCodeResponse, credential, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_totp_code(self, async_client: AsyncKernel) -> None: response = await async_client.credentials.with_raw_response.totp_code( @@ -575,7 +575,7 @@ async def test_raw_response_totp_code(self, async_client: AsyncKernel) -> None: credential = await response.parse() assert_matches_type(CredentialTotpCodeResponse, credential, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_totp_code(self, async_client: AsyncKernel) -> None: async with async_client.credentials.with_streaming_response.totp_code( @@ -589,7 +589,7 @@ async def test_streaming_response_totp_code(self, async_client: AsyncKernel) -> assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_totp_code(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): diff --git a/tests/api_resources/test_deployments.py b/tests/api_resources/test_deployments.py index 6c3354ef..417616bf 100644 --- a/tests/api_resources/test_deployments.py +++ b/tests/api_resources/test_deployments.py @@ -22,13 +22,13 @@ class TestDeployments: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create(self, client: Kernel) -> None: deployment = client.deployments.create() assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create_with_all_params(self, client: Kernel) -> None: deployment = client.deployments.create( @@ -52,7 +52,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_create(self, client: Kernel) -> None: response = client.deployments.with_raw_response.create() @@ -62,7 +62,7 @@ def test_raw_response_create(self, client: Kernel) -> None: deployment = response.parse() assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_create(self, client: Kernel) -> None: with client.deployments.with_streaming_response.create() as response: @@ -74,7 +74,7 @@ def test_streaming_response_create(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_retrieve(self, client: Kernel) -> None: deployment = client.deployments.retrieve( @@ -82,7 +82,7 @@ def test_method_retrieve(self, client: Kernel) -> None: ) assert_matches_type(DeploymentRetrieveResponse, deployment, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_retrieve(self, client: Kernel) -> None: response = client.deployments.with_raw_response.retrieve( @@ -94,7 +94,7 @@ def test_raw_response_retrieve(self, client: Kernel) -> None: deployment = response.parse() assert_matches_type(DeploymentRetrieveResponse, deployment, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_retrieve(self, client: Kernel) -> None: with client.deployments.with_streaming_response.retrieve( @@ -108,7 +108,7 @@ def test_streaming_response_retrieve(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_retrieve(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -116,13 +116,13 @@ def test_path_params_retrieve(self, client: Kernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_list(self, client: Kernel) -> None: deployment = client.deployments.list() assert_matches_type(SyncOffsetPagination[DeploymentListResponse], deployment, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_list_with_all_params(self, client: Kernel) -> None: deployment = client.deployments.list( @@ -132,7 +132,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(SyncOffsetPagination[DeploymentListResponse], deployment, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_list(self, client: Kernel) -> None: response = client.deployments.with_raw_response.list() @@ -142,7 +142,7 @@ def test_raw_response_list(self, client: Kernel) -> None: deployment = response.parse() assert_matches_type(SyncOffsetPagination[DeploymentListResponse], deployment, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_list(self, client: Kernel) -> None: with client.deployments.with_streaming_response.list() as response: @@ -154,7 +154,7 @@ def test_streaming_response_list(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize def test_method_follow(self, client: Kernel) -> None: deployment_stream = client.deployments.follow( @@ -162,7 +162,7 @@ def test_method_follow(self, client: Kernel) -> None: ) deployment_stream.response.close() - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize def test_method_follow_with_all_params(self, client: Kernel) -> None: deployment_stream = client.deployments.follow( @@ -171,7 +171,7 @@ def test_method_follow_with_all_params(self, client: Kernel) -> None: ) deployment_stream.response.close() - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize def test_raw_response_follow(self, client: Kernel) -> None: response = client.deployments.with_raw_response.follow( @@ -182,7 +182,7 @@ def test_raw_response_follow(self, client: Kernel) -> None: stream = response.parse() stream.close() - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize def test_streaming_response_follow(self, client: Kernel) -> None: with client.deployments.with_streaming_response.follow( @@ -196,7 +196,7 @@ def test_streaming_response_follow(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize def test_path_params_follow(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -210,13 +210,13 @@ class TestAsyncDeployments: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create(self, async_client: AsyncKernel) -> None: deployment = await async_client.deployments.create() assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: deployment = await async_client.deployments.create( @@ -240,7 +240,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> ) assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_create(self, async_client: AsyncKernel) -> None: response = await async_client.deployments.with_raw_response.create() @@ -250,7 +250,7 @@ async def test_raw_response_create(self, async_client: AsyncKernel) -> None: deployment = await response.parse() assert_matches_type(DeploymentCreateResponse, deployment, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: async with async_client.deployments.with_streaming_response.create() as response: @@ -262,7 +262,7 @@ async def test_streaming_response_create(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_retrieve(self, async_client: AsyncKernel) -> None: deployment = await async_client.deployments.retrieve( @@ -270,7 +270,7 @@ async def test_method_retrieve(self, async_client: AsyncKernel) -> None: ) assert_matches_type(DeploymentRetrieveResponse, deployment, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: response = await async_client.deployments.with_raw_response.retrieve( @@ -282,7 +282,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: deployment = await response.parse() assert_matches_type(DeploymentRetrieveResponse, deployment, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: async with async_client.deployments.with_streaming_response.retrieve( @@ -296,7 +296,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> N assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -304,13 +304,13 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_list(self, async_client: AsyncKernel) -> None: deployment = await async_client.deployments.list() assert_matches_type(AsyncOffsetPagination[DeploymentListResponse], deployment, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: deployment = await async_client.deployments.list( @@ -320,7 +320,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N ) assert_matches_type(AsyncOffsetPagination[DeploymentListResponse], deployment, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_list(self, async_client: AsyncKernel) -> None: response = await async_client.deployments.with_raw_response.list() @@ -330,7 +330,7 @@ async def test_raw_response_list(self, async_client: AsyncKernel) -> None: deployment = await response.parse() assert_matches_type(AsyncOffsetPagination[DeploymentListResponse], deployment, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: async with async_client.deployments.with_streaming_response.list() as response: @@ -342,7 +342,7 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize async def test_method_follow(self, async_client: AsyncKernel) -> None: deployment_stream = await async_client.deployments.follow( @@ -350,7 +350,7 @@ async def test_method_follow(self, async_client: AsyncKernel) -> None: ) await deployment_stream.response.aclose() - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize async def test_method_follow_with_all_params(self, async_client: AsyncKernel) -> None: deployment_stream = await async_client.deployments.follow( @@ -359,7 +359,7 @@ async def test_method_follow_with_all_params(self, async_client: AsyncKernel) -> ) await deployment_stream.response.aclose() - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize async def test_raw_response_follow(self, async_client: AsyncKernel) -> None: response = await async_client.deployments.with_raw_response.follow( @@ -370,7 +370,7 @@ async def test_raw_response_follow(self, async_client: AsyncKernel) -> None: stream = await response.parse() await stream.close() - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize async def test_streaming_response_follow(self, async_client: AsyncKernel) -> None: async with async_client.deployments.with_streaming_response.follow( @@ -384,7 +384,7 @@ async def test_streaming_response_follow(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize async def test_path_params_follow(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): diff --git a/tests/api_resources/test_extensions.py b/tests/api_resources/test_extensions.py index 5d61f327..6f31f548 100644 --- a/tests/api_resources/test_extensions.py +++ b/tests/api_resources/test_extensions.py @@ -28,13 +28,13 @@ class TestExtensions: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_list(self, client: Kernel) -> None: extension = client.extensions.list() assert_matches_type(ExtensionListResponse, extension, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_list(self, client: Kernel) -> None: response = client.extensions.with_raw_response.list() @@ -44,7 +44,7 @@ def test_raw_response_list(self, client: Kernel) -> None: extension = response.parse() assert_matches_type(ExtensionListResponse, extension, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_list(self, client: Kernel) -> None: with client.extensions.with_streaming_response.list() as response: @@ -56,7 +56,7 @@ def test_streaming_response_list(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_delete(self, client: Kernel) -> None: extension = client.extensions.delete( @@ -64,7 +64,7 @@ def test_method_delete(self, client: Kernel) -> None: ) assert extension is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_delete(self, client: Kernel) -> None: response = client.extensions.with_raw_response.delete( @@ -76,7 +76,7 @@ def test_raw_response_delete(self, client: Kernel) -> None: extension = response.parse() assert extension is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_delete(self, client: Kernel) -> None: with client.extensions.with_streaming_response.delete( @@ -90,7 +90,7 @@ def test_streaming_response_delete(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_delete(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): @@ -203,7 +203,7 @@ def test_streaming_response_download_from_chrome_store(self, client: Kernel, res assert cast(Any, extension.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_upload(self, client: Kernel) -> None: extension = client.extensions.upload( @@ -211,7 +211,7 @@ def test_method_upload(self, client: Kernel) -> None: ) assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_upload_with_all_params(self, client: Kernel) -> None: extension = client.extensions.upload( @@ -220,7 +220,7 @@ def test_method_upload_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_upload(self, client: Kernel) -> None: response = client.extensions.with_raw_response.upload( @@ -232,7 +232,7 @@ def test_raw_response_upload(self, client: Kernel) -> None: extension = response.parse() assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_upload(self, client: Kernel) -> None: with client.extensions.with_streaming_response.upload( @@ -252,13 +252,13 @@ class TestAsyncExtensions: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_list(self, async_client: AsyncKernel) -> None: extension = await async_client.extensions.list() assert_matches_type(ExtensionListResponse, extension, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_list(self, async_client: AsyncKernel) -> None: response = await async_client.extensions.with_raw_response.list() @@ -268,7 +268,7 @@ async def test_raw_response_list(self, async_client: AsyncKernel) -> None: extension = await response.parse() assert_matches_type(ExtensionListResponse, extension, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: async with async_client.extensions.with_streaming_response.list() as response: @@ -280,7 +280,7 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_delete(self, async_client: AsyncKernel) -> None: extension = await async_client.extensions.delete( @@ -288,7 +288,7 @@ async def test_method_delete(self, async_client: AsyncKernel) -> None: ) assert extension is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: response = await async_client.extensions.with_raw_response.delete( @@ -300,7 +300,7 @@ async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: extension = await response.parse() assert extension is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None: async with async_client.extensions.with_streaming_response.delete( @@ -314,7 +314,7 @@ async def test_streaming_response_delete(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_delete(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): @@ -433,7 +433,7 @@ async def test_streaming_response_download_from_chrome_store( assert cast(Any, extension.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_upload(self, async_client: AsyncKernel) -> None: extension = await async_client.extensions.upload( @@ -441,7 +441,7 @@ async def test_method_upload(self, async_client: AsyncKernel) -> None: ) assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_upload_with_all_params(self, async_client: AsyncKernel) -> None: extension = await async_client.extensions.upload( @@ -450,7 +450,7 @@ async def test_method_upload_with_all_params(self, async_client: AsyncKernel) -> ) assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_upload(self, async_client: AsyncKernel) -> None: response = await async_client.extensions.with_raw_response.upload( @@ -462,7 +462,7 @@ async def test_raw_response_upload(self, async_client: AsyncKernel) -> None: extension = await response.parse() assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_upload(self, async_client: AsyncKernel) -> None: async with async_client.extensions.with_streaming_response.upload( diff --git a/tests/api_resources/test_invocations.py b/tests/api_resources/test_invocations.py index d870adce..b8c381ab 100644 --- a/tests/api_resources/test_invocations.py +++ b/tests/api_resources/test_invocations.py @@ -24,7 +24,7 @@ class TestInvocations: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create(self, client: Kernel) -> None: invocation = client.invocations.create( @@ -34,7 +34,7 @@ def test_method_create(self, client: Kernel) -> None: ) assert_matches_type(InvocationCreateResponse, invocation, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create_with_all_params(self, client: Kernel) -> None: invocation = client.invocations.create( @@ -47,7 +47,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(InvocationCreateResponse, invocation, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_create(self, client: Kernel) -> None: response = client.invocations.with_raw_response.create( @@ -61,7 +61,7 @@ def test_raw_response_create(self, client: Kernel) -> None: invocation = response.parse() assert_matches_type(InvocationCreateResponse, invocation, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_create(self, client: Kernel) -> None: with client.invocations.with_streaming_response.create( @@ -77,7 +77,7 @@ def test_streaming_response_create(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_retrieve(self, client: Kernel) -> None: invocation = client.invocations.retrieve( @@ -85,7 +85,7 @@ def test_method_retrieve(self, client: Kernel) -> None: ) assert_matches_type(InvocationRetrieveResponse, invocation, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_retrieve(self, client: Kernel) -> None: response = client.invocations.with_raw_response.retrieve( @@ -97,7 +97,7 @@ def test_raw_response_retrieve(self, client: Kernel) -> None: invocation = response.parse() assert_matches_type(InvocationRetrieveResponse, invocation, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_retrieve(self, client: Kernel) -> None: with client.invocations.with_streaming_response.retrieve( @@ -111,7 +111,7 @@ def test_streaming_response_retrieve(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_retrieve(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -119,7 +119,7 @@ def test_path_params_retrieve(self, client: Kernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_update(self, client: Kernel) -> None: invocation = client.invocations.update( @@ -128,7 +128,7 @@ def test_method_update(self, client: Kernel) -> None: ) assert_matches_type(InvocationUpdateResponse, invocation, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_update_with_all_params(self, client: Kernel) -> None: invocation = client.invocations.update( @@ -138,7 +138,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(InvocationUpdateResponse, invocation, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_update(self, client: Kernel) -> None: response = client.invocations.with_raw_response.update( @@ -151,7 +151,7 @@ def test_raw_response_update(self, client: Kernel) -> None: invocation = response.parse() assert_matches_type(InvocationUpdateResponse, invocation, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_update(self, client: Kernel) -> None: with client.invocations.with_streaming_response.update( @@ -166,7 +166,7 @@ def test_streaming_response_update(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_update(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -175,13 +175,13 @@ def test_path_params_update(self, client: Kernel) -> None: status="succeeded", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_list(self, client: Kernel) -> None: invocation = client.invocations.list() assert_matches_type(SyncOffsetPagination[InvocationListResponse], invocation, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_list_with_all_params(self, client: Kernel) -> None: invocation = client.invocations.list( @@ -196,7 +196,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(SyncOffsetPagination[InvocationListResponse], invocation, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_list(self, client: Kernel) -> None: response = client.invocations.with_raw_response.list() @@ -206,7 +206,7 @@ def test_raw_response_list(self, client: Kernel) -> None: invocation = response.parse() assert_matches_type(SyncOffsetPagination[InvocationListResponse], invocation, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_list(self, client: Kernel) -> None: with client.invocations.with_streaming_response.list() as response: @@ -218,7 +218,7 @@ def test_streaming_response_list(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_delete_browsers(self, client: Kernel) -> None: invocation = client.invocations.delete_browsers( @@ -226,7 +226,7 @@ def test_method_delete_browsers(self, client: Kernel) -> None: ) assert invocation is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_delete_browsers(self, client: Kernel) -> None: response = client.invocations.with_raw_response.delete_browsers( @@ -238,7 +238,7 @@ def test_raw_response_delete_browsers(self, client: Kernel) -> None: invocation = response.parse() assert invocation is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_delete_browsers(self, client: Kernel) -> None: with client.invocations.with_streaming_response.delete_browsers( @@ -252,7 +252,7 @@ def test_streaming_response_delete_browsers(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_delete_browsers(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -260,7 +260,7 @@ def test_path_params_delete_browsers(self, client: Kernel) -> None: "", ) - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize def test_method_follow(self, client: Kernel) -> None: invocation_stream = client.invocations.follow( @@ -268,7 +268,7 @@ def test_method_follow(self, client: Kernel) -> None: ) invocation_stream.response.close() - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize def test_method_follow_with_all_params(self, client: Kernel) -> None: invocation_stream = client.invocations.follow( @@ -277,7 +277,7 @@ def test_method_follow_with_all_params(self, client: Kernel) -> None: ) invocation_stream.response.close() - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize def test_raw_response_follow(self, client: Kernel) -> None: response = client.invocations.with_raw_response.follow( @@ -288,7 +288,7 @@ def test_raw_response_follow(self, client: Kernel) -> None: stream = response.parse() stream.close() - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize def test_streaming_response_follow(self, client: Kernel) -> None: with client.invocations.with_streaming_response.follow( @@ -302,7 +302,7 @@ def test_streaming_response_follow(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize def test_path_params_follow(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -310,7 +310,7 @@ def test_path_params_follow(self, client: Kernel) -> None: id="", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_list_browsers(self, client: Kernel) -> None: invocation = client.invocations.list_browsers( @@ -318,7 +318,7 @@ def test_method_list_browsers(self, client: Kernel) -> None: ) assert_matches_type(InvocationListBrowsersResponse, invocation, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_list_browsers(self, client: Kernel) -> None: response = client.invocations.with_raw_response.list_browsers( @@ -330,7 +330,7 @@ def test_raw_response_list_browsers(self, client: Kernel) -> None: invocation = response.parse() assert_matches_type(InvocationListBrowsersResponse, invocation, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_list_browsers(self, client: Kernel) -> None: with client.invocations.with_streaming_response.list_browsers( @@ -344,7 +344,7 @@ def test_streaming_response_list_browsers(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_list_browsers(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -358,7 +358,7 @@ class TestAsyncInvocations: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create(self, async_client: AsyncKernel) -> None: invocation = await async_client.invocations.create( @@ -368,7 +368,7 @@ async def test_method_create(self, async_client: AsyncKernel) -> None: ) assert_matches_type(InvocationCreateResponse, invocation, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: invocation = await async_client.invocations.create( @@ -381,7 +381,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> ) assert_matches_type(InvocationCreateResponse, invocation, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_create(self, async_client: AsyncKernel) -> None: response = await async_client.invocations.with_raw_response.create( @@ -395,7 +395,7 @@ async def test_raw_response_create(self, async_client: AsyncKernel) -> None: invocation = await response.parse() assert_matches_type(InvocationCreateResponse, invocation, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: async with async_client.invocations.with_streaming_response.create( @@ -411,7 +411,7 @@ async def test_streaming_response_create(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_retrieve(self, async_client: AsyncKernel) -> None: invocation = await async_client.invocations.retrieve( @@ -419,7 +419,7 @@ async def test_method_retrieve(self, async_client: AsyncKernel) -> None: ) assert_matches_type(InvocationRetrieveResponse, invocation, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: response = await async_client.invocations.with_raw_response.retrieve( @@ -431,7 +431,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: invocation = await response.parse() assert_matches_type(InvocationRetrieveResponse, invocation, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: async with async_client.invocations.with_streaming_response.retrieve( @@ -445,7 +445,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> N assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -453,7 +453,7 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_update(self, async_client: AsyncKernel) -> None: invocation = await async_client.invocations.update( @@ -462,7 +462,7 @@ async def test_method_update(self, async_client: AsyncKernel) -> None: ) assert_matches_type(InvocationUpdateResponse, invocation, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> None: invocation = await async_client.invocations.update( @@ -472,7 +472,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> ) assert_matches_type(InvocationUpdateResponse, invocation, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_update(self, async_client: AsyncKernel) -> None: response = await async_client.invocations.with_raw_response.update( @@ -485,7 +485,7 @@ async def test_raw_response_update(self, async_client: AsyncKernel) -> None: invocation = await response.parse() assert_matches_type(InvocationUpdateResponse, invocation, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_update(self, async_client: AsyncKernel) -> None: async with async_client.invocations.with_streaming_response.update( @@ -500,7 +500,7 @@ async def test_streaming_response_update(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_update(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -509,13 +509,13 @@ async def test_path_params_update(self, async_client: AsyncKernel) -> None: status="succeeded", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_list(self, async_client: AsyncKernel) -> None: invocation = await async_client.invocations.list() assert_matches_type(AsyncOffsetPagination[InvocationListResponse], invocation, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: invocation = await async_client.invocations.list( @@ -530,7 +530,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N ) assert_matches_type(AsyncOffsetPagination[InvocationListResponse], invocation, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_list(self, async_client: AsyncKernel) -> None: response = await async_client.invocations.with_raw_response.list() @@ -540,7 +540,7 @@ async def test_raw_response_list(self, async_client: AsyncKernel) -> None: invocation = await response.parse() assert_matches_type(AsyncOffsetPagination[InvocationListResponse], invocation, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: async with async_client.invocations.with_streaming_response.list() as response: @@ -552,7 +552,7 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_delete_browsers(self, async_client: AsyncKernel) -> None: invocation = await async_client.invocations.delete_browsers( @@ -560,7 +560,7 @@ async def test_method_delete_browsers(self, async_client: AsyncKernel) -> None: ) assert invocation is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_delete_browsers(self, async_client: AsyncKernel) -> None: response = await async_client.invocations.with_raw_response.delete_browsers( @@ -572,7 +572,7 @@ async def test_raw_response_delete_browsers(self, async_client: AsyncKernel) -> invocation = await response.parse() assert invocation is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_delete_browsers(self, async_client: AsyncKernel) -> None: async with async_client.invocations.with_streaming_response.delete_browsers( @@ -586,7 +586,7 @@ async def test_streaming_response_delete_browsers(self, async_client: AsyncKerne assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_delete_browsers(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -594,7 +594,7 @@ async def test_path_params_delete_browsers(self, async_client: AsyncKernel) -> N "", ) - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize async def test_method_follow(self, async_client: AsyncKernel) -> None: invocation_stream = await async_client.invocations.follow( @@ -602,7 +602,7 @@ async def test_method_follow(self, async_client: AsyncKernel) -> None: ) await invocation_stream.response.aclose() - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize async def test_method_follow_with_all_params(self, async_client: AsyncKernel) -> None: invocation_stream = await async_client.invocations.follow( @@ -611,7 +611,7 @@ async def test_method_follow_with_all_params(self, async_client: AsyncKernel) -> ) await invocation_stream.response.aclose() - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize async def test_raw_response_follow(self, async_client: AsyncKernel) -> None: response = await async_client.invocations.with_raw_response.follow( @@ -622,7 +622,7 @@ async def test_raw_response_follow(self, async_client: AsyncKernel) -> None: stream = await response.parse() await stream.close() - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize async def test_streaming_response_follow(self, async_client: AsyncKernel) -> None: async with async_client.invocations.with_streaming_response.follow( @@ -636,7 +636,7 @@ async def test_streaming_response_follow(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") @parametrize async def test_path_params_follow(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -644,7 +644,7 @@ async def test_path_params_follow(self, async_client: AsyncKernel) -> None: id="", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_list_browsers(self, async_client: AsyncKernel) -> None: invocation = await async_client.invocations.list_browsers( @@ -652,7 +652,7 @@ async def test_method_list_browsers(self, async_client: AsyncKernel) -> None: ) assert_matches_type(InvocationListBrowsersResponse, invocation, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_list_browsers(self, async_client: AsyncKernel) -> None: response = await async_client.invocations.with_raw_response.list_browsers( @@ -664,7 +664,7 @@ async def test_raw_response_list_browsers(self, async_client: AsyncKernel) -> No invocation = await response.parse() assert_matches_type(InvocationListBrowsersResponse, invocation, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_list_browsers(self, async_client: AsyncKernel) -> None: async with async_client.invocations.with_streaming_response.list_browsers( @@ -678,7 +678,7 @@ async def test_streaming_response_list_browsers(self, async_client: AsyncKernel) assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_list_browsers(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): diff --git a/tests/api_resources/test_profiles.py b/tests/api_resources/test_profiles.py index 6c978558..2024ec8a 100644 --- a/tests/api_resources/test_profiles.py +++ b/tests/api_resources/test_profiles.py @@ -25,13 +25,13 @@ class TestProfiles: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create(self, client: Kernel) -> None: profile = client.profiles.create() assert_matches_type(Profile, profile, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create_with_all_params(self, client: Kernel) -> None: profile = client.profiles.create( @@ -39,7 +39,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(Profile, profile, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_create(self, client: Kernel) -> None: response = client.profiles.with_raw_response.create() @@ -49,7 +49,7 @@ def test_raw_response_create(self, client: Kernel) -> None: profile = response.parse() assert_matches_type(Profile, profile, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_create(self, client: Kernel) -> None: with client.profiles.with_streaming_response.create() as response: @@ -61,7 +61,7 @@ def test_streaming_response_create(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_retrieve(self, client: Kernel) -> None: profile = client.profiles.retrieve( @@ -69,7 +69,7 @@ def test_method_retrieve(self, client: Kernel) -> None: ) assert_matches_type(Profile, profile, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_retrieve(self, client: Kernel) -> None: response = client.profiles.with_raw_response.retrieve( @@ -81,7 +81,7 @@ def test_raw_response_retrieve(self, client: Kernel) -> None: profile = response.parse() assert_matches_type(Profile, profile, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_retrieve(self, client: Kernel) -> None: with client.profiles.with_streaming_response.retrieve( @@ -95,7 +95,7 @@ def test_streaming_response_retrieve(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_retrieve(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): @@ -103,13 +103,13 @@ def test_path_params_retrieve(self, client: Kernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_list(self, client: Kernel) -> None: profile = client.profiles.list() assert_matches_type(ProfileListResponse, profile, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_list(self, client: Kernel) -> None: response = client.profiles.with_raw_response.list() @@ -119,7 +119,7 @@ def test_raw_response_list(self, client: Kernel) -> None: profile = response.parse() assert_matches_type(ProfileListResponse, profile, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_list(self, client: Kernel) -> None: with client.profiles.with_streaming_response.list() as response: @@ -131,7 +131,7 @@ def test_streaming_response_list(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_delete(self, client: Kernel) -> None: profile = client.profiles.delete( @@ -139,7 +139,7 @@ def test_method_delete(self, client: Kernel) -> None: ) assert profile is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_delete(self, client: Kernel) -> None: response = client.profiles.with_raw_response.delete( @@ -151,7 +151,7 @@ def test_raw_response_delete(self, client: Kernel) -> None: profile = response.parse() assert profile is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_delete(self, client: Kernel) -> None: with client.profiles.with_streaming_response.delete( @@ -165,7 +165,7 @@ def test_streaming_response_delete(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_delete(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): @@ -229,13 +229,13 @@ class TestAsyncProfiles: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create(self, async_client: AsyncKernel) -> None: profile = await async_client.profiles.create() assert_matches_type(Profile, profile, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: profile = await async_client.profiles.create( @@ -243,7 +243,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> ) assert_matches_type(Profile, profile, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_create(self, async_client: AsyncKernel) -> None: response = await async_client.profiles.with_raw_response.create() @@ -253,7 +253,7 @@ async def test_raw_response_create(self, async_client: AsyncKernel) -> None: profile = await response.parse() assert_matches_type(Profile, profile, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: async with async_client.profiles.with_streaming_response.create() as response: @@ -265,7 +265,7 @@ async def test_streaming_response_create(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_retrieve(self, async_client: AsyncKernel) -> None: profile = await async_client.profiles.retrieve( @@ -273,7 +273,7 @@ async def test_method_retrieve(self, async_client: AsyncKernel) -> None: ) assert_matches_type(Profile, profile, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: response = await async_client.profiles.with_raw_response.retrieve( @@ -285,7 +285,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: profile = await response.parse() assert_matches_type(Profile, profile, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: async with async_client.profiles.with_streaming_response.retrieve( @@ -299,7 +299,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> N assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): @@ -307,13 +307,13 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_list(self, async_client: AsyncKernel) -> None: profile = await async_client.profiles.list() assert_matches_type(ProfileListResponse, profile, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_list(self, async_client: AsyncKernel) -> None: response = await async_client.profiles.with_raw_response.list() @@ -323,7 +323,7 @@ async def test_raw_response_list(self, async_client: AsyncKernel) -> None: profile = await response.parse() assert_matches_type(ProfileListResponse, profile, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: async with async_client.profiles.with_streaming_response.list() as response: @@ -335,7 +335,7 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_delete(self, async_client: AsyncKernel) -> None: profile = await async_client.profiles.delete( @@ -343,7 +343,7 @@ async def test_method_delete(self, async_client: AsyncKernel) -> None: ) assert profile is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: response = await async_client.profiles.with_raw_response.delete( @@ -355,7 +355,7 @@ async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: profile = await response.parse() assert profile is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None: async with async_client.profiles.with_streaming_response.delete( @@ -369,7 +369,7 @@ async def test_streaming_response_delete(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_delete(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): diff --git a/tests/api_resources/test_proxies.py b/tests/api_resources/test_proxies.py index ed858e8d..3caabece 100644 --- a/tests/api_resources/test_proxies.py +++ b/tests/api_resources/test_proxies.py @@ -22,7 +22,7 @@ class TestProxies: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create(self, client: Kernel) -> None: proxy = client.proxies.create( @@ -30,7 +30,7 @@ def test_method_create(self, client: Kernel) -> None: ) assert_matches_type(ProxyCreateResponse, proxy, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create_with_all_params(self, client: Kernel) -> None: proxy = client.proxies.create( @@ -41,7 +41,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: ) assert_matches_type(ProxyCreateResponse, proxy, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_create(self, client: Kernel) -> None: response = client.proxies.with_raw_response.create( @@ -53,7 +53,7 @@ def test_raw_response_create(self, client: Kernel) -> None: proxy = response.parse() assert_matches_type(ProxyCreateResponse, proxy, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_create(self, client: Kernel) -> None: with client.proxies.with_streaming_response.create( @@ -67,7 +67,7 @@ def test_streaming_response_create(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_retrieve(self, client: Kernel) -> None: proxy = client.proxies.retrieve( @@ -75,7 +75,7 @@ def test_method_retrieve(self, client: Kernel) -> None: ) assert_matches_type(ProxyRetrieveResponse, proxy, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_retrieve(self, client: Kernel) -> None: response = client.proxies.with_raw_response.retrieve( @@ -87,7 +87,7 @@ def test_raw_response_retrieve(self, client: Kernel) -> None: proxy = response.parse() assert_matches_type(ProxyRetrieveResponse, proxy, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_retrieve(self, client: Kernel) -> None: with client.proxies.with_streaming_response.retrieve( @@ -101,7 +101,7 @@ def test_streaming_response_retrieve(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_retrieve(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -109,13 +109,13 @@ def test_path_params_retrieve(self, client: Kernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_list(self, client: Kernel) -> None: proxy = client.proxies.list() assert_matches_type(ProxyListResponse, proxy, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_list(self, client: Kernel) -> None: response = client.proxies.with_raw_response.list() @@ -125,7 +125,7 @@ def test_raw_response_list(self, client: Kernel) -> None: proxy = response.parse() assert_matches_type(ProxyListResponse, proxy, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_list(self, client: Kernel) -> None: with client.proxies.with_streaming_response.list() as response: @@ -137,7 +137,7 @@ def test_streaming_response_list(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_delete(self, client: Kernel) -> None: proxy = client.proxies.delete( @@ -145,7 +145,7 @@ def test_method_delete(self, client: Kernel) -> None: ) assert proxy is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_delete(self, client: Kernel) -> None: response = client.proxies.with_raw_response.delete( @@ -157,7 +157,7 @@ def test_raw_response_delete(self, client: Kernel) -> None: proxy = response.parse() assert proxy is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_delete(self, client: Kernel) -> None: with client.proxies.with_streaming_response.delete( @@ -171,7 +171,7 @@ def test_streaming_response_delete(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_delete(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -179,7 +179,7 @@ def test_path_params_delete(self, client: Kernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_check(self, client: Kernel) -> None: proxy = client.proxies.check( @@ -187,7 +187,7 @@ def test_method_check(self, client: Kernel) -> None: ) assert_matches_type(ProxyCheckResponse, proxy, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_check(self, client: Kernel) -> None: response = client.proxies.with_raw_response.check( @@ -199,7 +199,7 @@ def test_raw_response_check(self, client: Kernel) -> None: proxy = response.parse() assert_matches_type(ProxyCheckResponse, proxy, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_check(self, client: Kernel) -> None: with client.proxies.with_streaming_response.check( @@ -213,7 +213,7 @@ def test_streaming_response_check(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_check(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -227,7 +227,7 @@ class TestAsyncProxies: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create(self, async_client: AsyncKernel) -> None: proxy = await async_client.proxies.create( @@ -235,7 +235,7 @@ async def test_method_create(self, async_client: AsyncKernel) -> None: ) assert_matches_type(ProxyCreateResponse, proxy, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: proxy = await async_client.proxies.create( @@ -246,7 +246,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> ) assert_matches_type(ProxyCreateResponse, proxy, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_create(self, async_client: AsyncKernel) -> None: response = await async_client.proxies.with_raw_response.create( @@ -258,7 +258,7 @@ async def test_raw_response_create(self, async_client: AsyncKernel) -> None: proxy = await response.parse() assert_matches_type(ProxyCreateResponse, proxy, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: async with async_client.proxies.with_streaming_response.create( @@ -272,7 +272,7 @@ async def test_streaming_response_create(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_retrieve(self, async_client: AsyncKernel) -> None: proxy = await async_client.proxies.retrieve( @@ -280,7 +280,7 @@ async def test_method_retrieve(self, async_client: AsyncKernel) -> None: ) assert_matches_type(ProxyRetrieveResponse, proxy, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: response = await async_client.proxies.with_raw_response.retrieve( @@ -292,7 +292,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: proxy = await response.parse() assert_matches_type(ProxyRetrieveResponse, proxy, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: async with async_client.proxies.with_streaming_response.retrieve( @@ -306,7 +306,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> N assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -314,13 +314,13 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_list(self, async_client: AsyncKernel) -> None: proxy = await async_client.proxies.list() assert_matches_type(ProxyListResponse, proxy, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_list(self, async_client: AsyncKernel) -> None: response = await async_client.proxies.with_raw_response.list() @@ -330,7 +330,7 @@ async def test_raw_response_list(self, async_client: AsyncKernel) -> None: proxy = await response.parse() assert_matches_type(ProxyListResponse, proxy, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: async with async_client.proxies.with_streaming_response.list() as response: @@ -342,7 +342,7 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_delete(self, async_client: AsyncKernel) -> None: proxy = await async_client.proxies.delete( @@ -350,7 +350,7 @@ async def test_method_delete(self, async_client: AsyncKernel) -> None: ) assert proxy is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: response = await async_client.proxies.with_raw_response.delete( @@ -362,7 +362,7 @@ async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: proxy = await response.parse() assert proxy is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None: async with async_client.proxies.with_streaming_response.delete( @@ -376,7 +376,7 @@ async def test_streaming_response_delete(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_delete(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -384,7 +384,7 @@ async def test_path_params_delete(self, async_client: AsyncKernel) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_check(self, async_client: AsyncKernel) -> None: proxy = await async_client.proxies.check( @@ -392,7 +392,7 @@ async def test_method_check(self, async_client: AsyncKernel) -> None: ) assert_matches_type(ProxyCheckResponse, proxy, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_check(self, async_client: AsyncKernel) -> None: response = await async_client.proxies.with_raw_response.check( @@ -404,7 +404,7 @@ async def test_raw_response_check(self, async_client: AsyncKernel) -> None: proxy = await response.parse() assert_matches_type(ProxyCheckResponse, proxy, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_check(self, async_client: AsyncKernel) -> None: async with async_client.proxies.with_streaming_response.check( @@ -418,7 +418,7 @@ async def test_streaming_response_check(self, async_client: AsyncKernel) -> None assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_check(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): From cc41ca1e872564c1863b93e921757634cf48b4f9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 03:27:43 +0000 Subject: [PATCH 306/448] chore(test): update skip reason message --- tests/api_resources/auth/test_connections.py | 16 +++++++-------- tests/api_resources/browsers/fs/test_watch.py | 16 +++++++-------- tests/api_resources/browsers/test_logs.py | 20 +++++++++---------- tests/api_resources/browsers/test_process.py | 16 +++++++-------- tests/api_resources/test_deployments.py | 20 +++++++++---------- tests/api_resources/test_invocations.py | 20 +++++++++---------- 6 files changed, 54 insertions(+), 54 deletions(-) diff --git a/tests/api_resources/auth/test_connections.py b/tests/api_resources/auth/test_connections.py index 2e208663..e71a1736 100644 --- a/tests/api_resources/auth/test_connections.py +++ b/tests/api_resources/auth/test_connections.py @@ -205,7 +205,7 @@ def test_path_params_delete(self, client: Kernel) -> None: "", ) - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_follow(self, client: Kernel) -> None: connection_stream = client.auth.connections.follow( @@ -213,7 +213,7 @@ def test_method_follow(self, client: Kernel) -> None: ) connection_stream.response.close() - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_follow(self, client: Kernel) -> None: response = client.auth.connections.with_raw_response.follow( @@ -224,7 +224,7 @@ def test_raw_response_follow(self, client: Kernel) -> None: stream = response.parse() stream.close() - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_follow(self, client: Kernel) -> None: with client.auth.connections.with_streaming_response.follow( @@ -238,7 +238,7 @@ def test_streaming_response_follow(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_follow(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -545,7 +545,7 @@ async def test_path_params_delete(self, async_client: AsyncKernel) -> None: "", ) - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_follow(self, async_client: AsyncKernel) -> None: connection_stream = await async_client.auth.connections.follow( @@ -553,7 +553,7 @@ async def test_method_follow(self, async_client: AsyncKernel) -> None: ) await connection_stream.response.aclose() - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_follow(self, async_client: AsyncKernel) -> None: response = await async_client.auth.connections.with_raw_response.follow( @@ -564,7 +564,7 @@ async def test_raw_response_follow(self, async_client: AsyncKernel) -> None: stream = await response.parse() await stream.close() - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_follow(self, async_client: AsyncKernel) -> None: async with async_client.auth.connections.with_streaming_response.follow( @@ -578,7 +578,7 @@ async def test_streaming_response_follow(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_follow(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): diff --git a/tests/api_resources/browsers/fs/test_watch.py b/tests/api_resources/browsers/fs/test_watch.py index 48b3dcf5..b28086ba 100644 --- a/tests/api_resources/browsers/fs/test_watch.py +++ b/tests/api_resources/browsers/fs/test_watch.py @@ -17,7 +17,7 @@ class TestWatch: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_events(self, client: Kernel) -> None: watch_stream = client.browsers.fs.watch.events( @@ -26,7 +26,7 @@ def test_method_events(self, client: Kernel) -> None: ) watch_stream.response.close() - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_events(self, client: Kernel) -> None: response = client.browsers.fs.watch.with_raw_response.events( @@ -38,7 +38,7 @@ def test_raw_response_events(self, client: Kernel) -> None: stream = response.parse() stream.close() - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_events(self, client: Kernel) -> None: with client.browsers.fs.watch.with_streaming_response.events( @@ -53,7 +53,7 @@ def test_streaming_response_events(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_events(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -182,7 +182,7 @@ class TestAsyncWatch: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_events(self, async_client: AsyncKernel) -> None: watch_stream = await async_client.browsers.fs.watch.events( @@ -191,7 +191,7 @@ async def test_method_events(self, async_client: AsyncKernel) -> None: ) await watch_stream.response.aclose() - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_events(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.fs.watch.with_raw_response.events( @@ -203,7 +203,7 @@ async def test_raw_response_events(self, async_client: AsyncKernel) -> None: stream = await response.parse() await stream.close() - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_events(self, async_client: AsyncKernel) -> None: async with async_client.browsers.fs.watch.with_streaming_response.events( @@ -218,7 +218,7 @@ async def test_streaming_response_events(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_events(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): diff --git a/tests/api_resources/browsers/test_logs.py b/tests/api_resources/browsers/test_logs.py index 8048908d..27268b82 100644 --- a/tests/api_resources/browsers/test_logs.py +++ b/tests/api_resources/browsers/test_logs.py @@ -15,7 +15,7 @@ class TestLogs: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_stream(self, client: Kernel) -> None: log_stream = client.browsers.logs.stream( @@ -24,7 +24,7 @@ def test_method_stream(self, client: Kernel) -> None: ) log_stream.response.close() - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_stream_with_all_params(self, client: Kernel) -> None: log_stream = client.browsers.logs.stream( @@ -36,7 +36,7 @@ def test_method_stream_with_all_params(self, client: Kernel) -> None: ) log_stream.response.close() - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_stream(self, client: Kernel) -> None: response = client.browsers.logs.with_raw_response.stream( @@ -48,7 +48,7 @@ def test_raw_response_stream(self, client: Kernel) -> None: stream = response.parse() stream.close() - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_stream(self, client: Kernel) -> None: with client.browsers.logs.with_streaming_response.stream( @@ -63,7 +63,7 @@ def test_streaming_response_stream(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_stream(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -78,7 +78,7 @@ class TestAsyncLogs: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_stream(self, async_client: AsyncKernel) -> None: log_stream = await async_client.browsers.logs.stream( @@ -87,7 +87,7 @@ async def test_method_stream(self, async_client: AsyncKernel) -> None: ) await log_stream.response.aclose() - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_stream_with_all_params(self, async_client: AsyncKernel) -> None: log_stream = await async_client.browsers.logs.stream( @@ -99,7 +99,7 @@ async def test_method_stream_with_all_params(self, async_client: AsyncKernel) -> ) await log_stream.response.aclose() - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_stream(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.logs.with_raw_response.stream( @@ -111,7 +111,7 @@ async def test_raw_response_stream(self, async_client: AsyncKernel) -> None: stream = await response.parse() await stream.close() - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_stream(self, async_client: AsyncKernel) -> None: async with async_client.browsers.logs.with_streaming_response.stream( @@ -126,7 +126,7 @@ async def test_streaming_response_stream(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_stream(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): diff --git a/tests/api_resources/browsers/test_process.py b/tests/api_resources/browsers/test_process.py index 1fc71335..0418d219 100644 --- a/tests/api_resources/browsers/test_process.py +++ b/tests/api_resources/browsers/test_process.py @@ -377,7 +377,7 @@ def test_path_params_stdin(self, client: Kernel) -> None: data_b64="data_b64", ) - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_stdout_stream(self, client: Kernel) -> None: process_stream = client.browsers.process.stdout_stream( @@ -386,7 +386,7 @@ def test_method_stdout_stream(self, client: Kernel) -> None: ) process_stream.response.close() - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_stdout_stream(self, client: Kernel) -> None: response = client.browsers.process.with_raw_response.stdout_stream( @@ -398,7 +398,7 @@ def test_raw_response_stdout_stream(self, client: Kernel) -> None: stream = response.parse() stream.close() - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_stdout_stream(self, client: Kernel) -> None: with client.browsers.process.with_streaming_response.stdout_stream( @@ -413,7 +413,7 @@ def test_streaming_response_stdout_stream(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_stdout_stream(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -787,7 +787,7 @@ async def test_path_params_stdin(self, async_client: AsyncKernel) -> None: data_b64="data_b64", ) - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_stdout_stream(self, async_client: AsyncKernel) -> None: process_stream = await async_client.browsers.process.stdout_stream( @@ -796,7 +796,7 @@ async def test_method_stdout_stream(self, async_client: AsyncKernel) -> None: ) await process_stream.response.aclose() - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_stdout_stream(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.process.with_raw_response.stdout_stream( @@ -808,7 +808,7 @@ async def test_raw_response_stdout_stream(self, async_client: AsyncKernel) -> No stream = await response.parse() await stream.close() - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_stdout_stream(self, async_client: AsyncKernel) -> None: async with async_client.browsers.process.with_streaming_response.stdout_stream( @@ -823,7 +823,7 @@ async def test_streaming_response_stdout_stream(self, async_client: AsyncKernel) assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_stdout_stream(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): diff --git a/tests/api_resources/test_deployments.py b/tests/api_resources/test_deployments.py index 417616bf..d1cbf700 100644 --- a/tests/api_resources/test_deployments.py +++ b/tests/api_resources/test_deployments.py @@ -154,7 +154,7 @@ def test_streaming_response_list(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_follow(self, client: Kernel) -> None: deployment_stream = client.deployments.follow( @@ -162,7 +162,7 @@ def test_method_follow(self, client: Kernel) -> None: ) deployment_stream.response.close() - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_follow_with_all_params(self, client: Kernel) -> None: deployment_stream = client.deployments.follow( @@ -171,7 +171,7 @@ def test_method_follow_with_all_params(self, client: Kernel) -> None: ) deployment_stream.response.close() - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_follow(self, client: Kernel) -> None: response = client.deployments.with_raw_response.follow( @@ -182,7 +182,7 @@ def test_raw_response_follow(self, client: Kernel) -> None: stream = response.parse() stream.close() - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_follow(self, client: Kernel) -> None: with client.deployments.with_streaming_response.follow( @@ -196,7 +196,7 @@ def test_streaming_response_follow(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_follow(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -342,7 +342,7 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_follow(self, async_client: AsyncKernel) -> None: deployment_stream = await async_client.deployments.follow( @@ -350,7 +350,7 @@ async def test_method_follow(self, async_client: AsyncKernel) -> None: ) await deployment_stream.response.aclose() - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_follow_with_all_params(self, async_client: AsyncKernel) -> None: deployment_stream = await async_client.deployments.follow( @@ -359,7 +359,7 @@ async def test_method_follow_with_all_params(self, async_client: AsyncKernel) -> ) await deployment_stream.response.aclose() - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_follow(self, async_client: AsyncKernel) -> None: response = await async_client.deployments.with_raw_response.follow( @@ -370,7 +370,7 @@ async def test_raw_response_follow(self, async_client: AsyncKernel) -> None: stream = await response.parse() await stream.close() - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_follow(self, async_client: AsyncKernel) -> None: async with async_client.deployments.with_streaming_response.follow( @@ -384,7 +384,7 @@ async def test_streaming_response_follow(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_follow(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): diff --git a/tests/api_resources/test_invocations.py b/tests/api_resources/test_invocations.py index b8c381ab..d2784c21 100644 --- a/tests/api_resources/test_invocations.py +++ b/tests/api_resources/test_invocations.py @@ -260,7 +260,7 @@ def test_path_params_delete_browsers(self, client: Kernel) -> None: "", ) - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_follow(self, client: Kernel) -> None: invocation_stream = client.invocations.follow( @@ -268,7 +268,7 @@ def test_method_follow(self, client: Kernel) -> None: ) invocation_stream.response.close() - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_follow_with_all_params(self, client: Kernel) -> None: invocation_stream = client.invocations.follow( @@ -277,7 +277,7 @@ def test_method_follow_with_all_params(self, client: Kernel) -> None: ) invocation_stream.response.close() - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_follow(self, client: Kernel) -> None: response = client.invocations.with_raw_response.follow( @@ -288,7 +288,7 @@ def test_raw_response_follow(self, client: Kernel) -> None: stream = response.parse() stream.close() - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_follow(self, client: Kernel) -> None: with client.invocations.with_streaming_response.follow( @@ -302,7 +302,7 @@ def test_streaming_response_follow(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_follow(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -594,7 +594,7 @@ async def test_path_params_delete_browsers(self, async_client: AsyncKernel) -> N "", ) - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_follow(self, async_client: AsyncKernel) -> None: invocation_stream = await async_client.invocations.follow( @@ -602,7 +602,7 @@ async def test_method_follow(self, async_client: AsyncKernel) -> None: ) await invocation_stream.response.aclose() - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_follow_with_all_params(self, async_client: AsyncKernel) -> None: invocation_stream = await async_client.invocations.follow( @@ -611,7 +611,7 @@ async def test_method_follow_with_all_params(self, async_client: AsyncKernel) -> ) await invocation_stream.response.aclose() - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_follow(self, async_client: AsyncKernel) -> None: response = await async_client.invocations.with_raw_response.follow( @@ -622,7 +622,7 @@ async def test_raw_response_follow(self, async_client: AsyncKernel) -> None: stream = await response.parse() await stream.close() - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_follow(self, async_client: AsyncKernel) -> None: async with async_client.invocations.with_streaming_response.follow( @@ -636,7 +636,7 @@ async def test_streaming_response_follow(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Mock server doesn't support text/event-stream responses") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_follow(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): From 5b2629a09cf60ac84dbdaf719c303e0e10294f3a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 21 Feb 2026 18:10:31 +0000 Subject: [PATCH 307/448] feat: Add DELETE /deployments/{id} API endpoint --- .stats.yml | 8 +-- api.md | 1 + src/kernel/resources/deployments.py | 86 ++++++++++++++++++++++++- tests/api_resources/test_deployments.py | 84 ++++++++++++++++++++++++ 4 files changed, 174 insertions(+), 5 deletions(-) diff --git a/.stats.yml b/.stats.yml index 92d7f1dc..be750ebe 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 100 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-a0f1d08e6f62a74de2aac5c25e592494abdd59f2cfca2842c5810927554faee0.yml -openapi_spec_hash: ebd8bf67b7bb371cf4b4fa68b967cab5 -config_hash: 27c0ea01aeb797a1767af139851c5b66 +configured_endpoints: 101 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-bbc3dbdd0410eb315cfaeb21aad9f85e4a7f92ac55526ebb702a8bee343c2ab7.yml +openapi_spec_hash: 60a5134c45a8f3a217e128d4e3335cae +config_hash: 147340811dd6fbb9c2d80515a7e31f9a diff --git a/api.md b/api.md index e76371c3..45197699 100644 --- a/api.md +++ b/api.md @@ -33,6 +33,7 @@ Methods: - client.deployments.create(\*\*params) -> DeploymentCreateResponse - client.deployments.retrieve(id) -> DeploymentRetrieveResponse - client.deployments.list(\*\*params) -> SyncOffsetPagination[DeploymentListResponse] +- client.deployments.delete(id) -> None - client.deployments.follow(id, \*\*params) -> DeploymentFollowResponse # Apps diff --git a/src/kernel/resources/deployments.py b/src/kernel/resources/deployments.py index f924531c..06e28877 100644 --- a/src/kernel/resources/deployments.py +++ b/src/kernel/resources/deployments.py @@ -8,7 +8,7 @@ import httpx from ..types import deployment_list_params, deployment_create_params, deployment_follow_params -from .._types import Body, Omit, Query, Headers, NotGiven, FileTypes, omit, not_given +from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, FileTypes, omit, not_given from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -204,6 +204,42 @@ def list( model=DeploymentListResponse, ) + def delete( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """Stops a running deployment and marks it for deletion. + + If the deployment is + already in a terminal state (stopped or failed), returns immediately. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + f"/deployments/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + def follow( self, id: str, @@ -427,6 +463,42 @@ def list( model=DeploymentListResponse, ) + async def delete( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """Stops a running deployment and marks it for deletion. + + If the deployment is + already in a terminal state (stopped or failed), returns immediately. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + f"/deployments/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + async def follow( self, id: str, @@ -488,6 +560,9 @@ def __init__(self, deployments: DeploymentsResource) -> None: self.list = to_raw_response_wrapper( deployments.list, ) + self.delete = to_raw_response_wrapper( + deployments.delete, + ) self.follow = to_raw_response_wrapper( deployments.follow, ) @@ -506,6 +581,9 @@ def __init__(self, deployments: AsyncDeploymentsResource) -> None: self.list = async_to_raw_response_wrapper( deployments.list, ) + self.delete = async_to_raw_response_wrapper( + deployments.delete, + ) self.follow = async_to_raw_response_wrapper( deployments.follow, ) @@ -524,6 +602,9 @@ def __init__(self, deployments: DeploymentsResource) -> None: self.list = to_streamed_response_wrapper( deployments.list, ) + self.delete = to_streamed_response_wrapper( + deployments.delete, + ) self.follow = to_streamed_response_wrapper( deployments.follow, ) @@ -542,6 +623,9 @@ def __init__(self, deployments: AsyncDeploymentsResource) -> None: self.list = async_to_streamed_response_wrapper( deployments.list, ) + self.delete = async_to_streamed_response_wrapper( + deployments.delete, + ) self.follow = async_to_streamed_response_wrapper( deployments.follow, ) diff --git a/tests/api_resources/test_deployments.py b/tests/api_resources/test_deployments.py index d1cbf700..d98ac242 100644 --- a/tests/api_resources/test_deployments.py +++ b/tests/api_resources/test_deployments.py @@ -154,6 +154,48 @@ def test_streaming_response_list(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_delete(self, client: Kernel) -> None: + deployment = client.deployments.delete( + "id", + ) + assert deployment is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_delete(self, client: Kernel) -> None: + response = client.deployments.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + deployment = response.parse() + assert deployment is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_delete(self, client: Kernel) -> None: + with client.deployments.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + deployment = response.parse() + assert deployment is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_delete(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.deployments.with_raw_response.delete( + "", + ) + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_follow(self, client: Kernel) -> None: @@ -342,6 +384,48 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: assert cast(Any, response.is_closed) is True + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_delete(self, async_client: AsyncKernel) -> None: + deployment = await async_client.deployments.delete( + "id", + ) + assert deployment is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: + response = await async_client.deployments.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + deployment = await response.parse() + assert deployment is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None: + async with async_client.deployments.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + deployment = await response.parse() + assert deployment is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_delete(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.deployments.with_raw_response.delete( + "", + ) + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_follow(self, async_client: AsyncKernel) -> None: From 47daeb4c91a72d23d4f56a270668565f71d4ecda Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 21 Feb 2026 18:18:26 +0000 Subject: [PATCH 308/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index ce5e5c7c..157f0355 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.35.0" + ".": "0.36.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 75cbfbee..ecebc6fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.35.0" +version = "0.36.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index ccc3c759..a4c76b0e 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.35.0" # x-release-please-version +__version__ = "0.36.0" # x-release-please-version From 7ac69d62cd72442b09dbdfe6f5e80864ade215ae Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 21 Feb 2026 20:54:00 +0000 Subject: [PATCH 309/448] feat: Add version filter to GET /deployments endpoint --- .stats.yml | 4 ++-- src/kernel/resources/deployments.py | 12 ++++++++++-- src/kernel/types/deployment_list_params.py | 3 +++ tests/api_resources/test_deployments.py | 2 ++ 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/.stats.yml b/.stats.yml index be750ebe..c875efdd 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 101 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-bbc3dbdd0410eb315cfaeb21aad9f85e4a7f92ac55526ebb702a8bee343c2ab7.yml -openapi_spec_hash: 60a5134c45a8f3a217e128d4e3335cae +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-ea5c9cb25c29fa5a8758bbf8732eb306783bb6f13b4df29bf1ad5ad3cb32da1e.yml +openapi_spec_hash: 597031840469b011f5cf22a4d8b9d750 config_hash: 147340811dd6fbb9c2d80515a7e31f9a diff --git a/src/kernel/resources/deployments.py b/src/kernel/resources/deployments.py index 06e28877..753eb8b1 100644 --- a/src/kernel/resources/deployments.py +++ b/src/kernel/resources/deployments.py @@ -156,6 +156,7 @@ def list( self, *, app_name: str | Omit = omit, + app_version: str | Omit = omit, limit: int | Omit = omit, offset: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -167,11 +168,13 @@ def list( ) -> SyncOffsetPagination[DeploymentListResponse]: """List deployments. - Optionally filter by application name. + Optionally filter by application name and version. Args: app_name: Filter results by application name. + app_version: Filter results by application version. Requires app_name to be set. + limit: Limit the number of deployments to return. offset: Offset the number of deployments to return. @@ -195,6 +198,7 @@ def list( query=maybe_transform( { "app_name": app_name, + "app_version": app_version, "limit": limit, "offset": offset, }, @@ -415,6 +419,7 @@ def list( self, *, app_name: str | Omit = omit, + app_version: str | Omit = omit, limit: int | Omit = omit, offset: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -426,11 +431,13 @@ def list( ) -> AsyncPaginator[DeploymentListResponse, AsyncOffsetPagination[DeploymentListResponse]]: """List deployments. - Optionally filter by application name. + Optionally filter by application name and version. Args: app_name: Filter results by application name. + app_version: Filter results by application version. Requires app_name to be set. + limit: Limit the number of deployments to return. offset: Offset the number of deployments to return. @@ -454,6 +461,7 @@ def list( query=maybe_transform( { "app_name": app_name, + "app_version": app_version, "limit": limit, "offset": offset, }, diff --git a/src/kernel/types/deployment_list_params.py b/src/kernel/types/deployment_list_params.py index 54124da5..4c1c0717 100644 --- a/src/kernel/types/deployment_list_params.py +++ b/src/kernel/types/deployment_list_params.py @@ -11,6 +11,9 @@ class DeploymentListParams(TypedDict, total=False): app_name: str """Filter results by application name.""" + app_version: str + """Filter results by application version. Requires app_name to be set.""" + limit: int """Limit the number of deployments to return.""" diff --git a/tests/api_resources/test_deployments.py b/tests/api_resources/test_deployments.py index d98ac242..25ad439e 100644 --- a/tests/api_resources/test_deployments.py +++ b/tests/api_resources/test_deployments.py @@ -127,6 +127,7 @@ def test_method_list(self, client: Kernel) -> None: def test_method_list_with_all_params(self, client: Kernel) -> None: deployment = client.deployments.list( app_name="app_name", + app_version="app_version", limit=1, offset=0, ) @@ -357,6 +358,7 @@ async def test_method_list(self, async_client: AsyncKernel) -> None: async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: deployment = await async_client.deployments.list( app_name="app_name", + app_version="app_version", limit=1, offset=0, ) From 97c0ac414197e2f3ea80a5d82f03dd3edbe6c0f9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 21 Feb 2026 20:56:50 +0000 Subject: [PATCH 310/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 157f0355..559b4513 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.36.0" + ".": "0.36.1" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index ecebc6fd..f969dc23 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.36.0" +version = "0.36.1" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index a4c76b0e..eca0c818 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.36.0" # x-release-please-version +__version__ = "0.36.1" # x-release-please-version From f543253b088acecd8bf80707853e58b7bb79232c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 16:39:07 +0000 Subject: [PATCH 311/448] feat: Neil/kernel 1017 profile pagination query parameter --- .stats.yml | 6 +- api.md | 8 +- src/kernel/resources/profiles.py | 92 +++++++++++++++++++---- src/kernel/types/__init__.py | 2 +- src/kernel/types/profile_list_params.py | 18 +++++ src/kernel/types/profile_list_response.py | 10 --- tests/api_resources/test_profiles.py | 35 +++++++-- 7 files changed, 129 insertions(+), 42 deletions(-) create mode 100644 src/kernel/types/profile_list_params.py delete mode 100644 src/kernel/types/profile_list_response.py diff --git a/.stats.yml b/.stats.yml index c875efdd..b154e7ae 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 101 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-ea5c9cb25c29fa5a8758bbf8732eb306783bb6f13b4df29bf1ad5ad3cb32da1e.yml -openapi_spec_hash: 597031840469b011f5cf22a4d8b9d750 -config_hash: 147340811dd6fbb9c2d80515a7e31f9a +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-fc4a441d80d9a26574ef8af390a0c76265f5d4190daf90a04b6b353b128bbd97.yml +openapi_spec_hash: 192987649d3797c3a80e6ef201667b64 +config_hash: 8af430e19f4af86c05f2987241cae72f diff --git a/api.md b/api.md index 45197699..0cda6cac 100644 --- a/api.md +++ b/api.md @@ -221,17 +221,11 @@ Methods: # Profiles -Types: - -```python -from kernel.types import ProfileListResponse -``` - Methods: - client.profiles.create(\*\*params) -> Profile - client.profiles.retrieve(id_or_name) -> Profile -- client.profiles.list() -> ProfileListResponse +- client.profiles.list(\*\*params) -> SyncOffsetPagination[Profile] - client.profiles.delete(id_or_name) -> None - client.profiles.download(id_or_name) -> BinaryAPIResponse diff --git a/src/kernel/resources/profiles.py b/src/kernel/resources/profiles.py index 86064d52..e9b124b5 100644 --- a/src/kernel/resources/profiles.py +++ b/src/kernel/resources/profiles.py @@ -4,7 +4,7 @@ import httpx -from ..types import profile_create_params +from ..types import profile_list_params, profile_create_params from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property @@ -23,9 +23,9 @@ async_to_custom_raw_response_wrapper, async_to_custom_streamed_response_wrapper, ) -from .._base_client import make_request_options +from ..pagination import SyncOffsetPagination, AsyncOffsetPagination +from .._base_client import AsyncPaginator, make_request_options from ..types.profile import Profile -from ..types.profile_list_response import ProfileListResponse __all__ = ["ProfilesResource", "AsyncProfilesResource"] @@ -121,20 +121,52 @@ def retrieve( def list( self, *, + limit: int | Omit = omit, + offset: int | Omit = omit, + query: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ProfileListResponse: - """List profiles with optional filtering and pagination.""" - return self._get( + ) -> SyncOffsetPagination[Profile]: + """ + List profiles with optional filtering and pagination. + + Args: + limit: Limit the number of profiles to return. + + offset: Offset the number of profiles to return. + + query: Search profiles by name or ID. + + 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_api_list( "/profiles", + page=SyncOffsetPagination[Profile], options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "offset": offset, + "query": query, + }, + profile_list_params.ProfileListParams, + ), ), - cast_to=ProfileListResponse, + model=Profile, ) def delete( @@ -296,23 +328,55 @@ async def retrieve( cast_to=Profile, ) - async def list( + def list( self, *, + limit: int | Omit = omit, + offset: int | Omit = omit, + query: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ProfileListResponse: - """List profiles with optional filtering and pagination.""" - return await self._get( + ) -> AsyncPaginator[Profile, AsyncOffsetPagination[Profile]]: + """ + List profiles with optional filtering and pagination. + + Args: + limit: Limit the number of profiles to return. + + offset: Offset the number of profiles to return. + + query: Search profiles by name or ID. + + 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_api_list( "/profiles", + page=AsyncOffsetPagination[Profile], options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "offset": offset, + "query": query, + }, + profile_list_params.ProfileListParams, + ), ), - cast_to=ProfileListResponse, + model=Profile, ) async def delete( diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 6340848f..2c3b4a8f 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -21,6 +21,7 @@ from .browser_list_params import BrowserListParams as BrowserListParams from .browser_persistence import BrowserPersistence as BrowserPersistence from .credential_provider import CredentialProvider as CredentialProvider +from .profile_list_params import ProfileListParams as ProfileListParams from .proxy_create_params import ProxyCreateParams as ProxyCreateParams from .proxy_list_response import ProxyListResponse as ProxyListResponse from .proxy_check_response import ProxyCheckResponse as ProxyCheckResponse @@ -29,7 +30,6 @@ from .browser_list_response import BrowserListResponse as BrowserListResponse from .browser_update_params import BrowserUpdateParams as BrowserUpdateParams from .profile_create_params import ProfileCreateParams as ProfileCreateParams -from .profile_list_response import ProfileListResponse as ProfileListResponse from .proxy_create_response import ProxyCreateResponse as ProxyCreateResponse from .credential_list_params import CredentialListParams as CredentialListParams from .deployment_list_params import DeploymentListParams as DeploymentListParams diff --git a/src/kernel/types/profile_list_params.py b/src/kernel/types/profile_list_params.py new file mode 100644 index 00000000..4218f5bc --- /dev/null +++ b/src/kernel/types/profile_list_params.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["ProfileListParams"] + + +class ProfileListParams(TypedDict, total=False): + limit: int + """Limit the number of profiles to return.""" + + offset: int + """Offset the number of profiles to return.""" + + query: str + """Search profiles by name or ID.""" diff --git a/src/kernel/types/profile_list_response.py b/src/kernel/types/profile_list_response.py deleted file mode 100644 index 24b2744c..00000000 --- a/src/kernel/types/profile_list_response.py +++ /dev/null @@ -1,10 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List -from typing_extensions import TypeAlias - -from .profile import Profile - -__all__ = ["ProfileListResponse"] - -ProfileListResponse: TypeAlias = List[Profile] diff --git a/tests/api_resources/test_profiles.py b/tests/api_resources/test_profiles.py index 2024ec8a..296593ea 100644 --- a/tests/api_resources/test_profiles.py +++ b/tests/api_resources/test_profiles.py @@ -11,13 +11,14 @@ from kernel import Kernel, AsyncKernel from tests.utils import assert_matches_type -from kernel.types import Profile, ProfileListResponse +from kernel.types import Profile from kernel._response import ( BinaryAPIResponse, AsyncBinaryAPIResponse, StreamedBinaryAPIResponse, AsyncStreamedBinaryAPIResponse, ) +from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -107,7 +108,17 @@ def test_path_params_retrieve(self, client: Kernel) -> None: @parametrize def test_method_list(self, client: Kernel) -> None: profile = client.profiles.list() - assert_matches_type(ProfileListResponse, profile, path=["response"]) + assert_matches_type(SyncOffsetPagination[Profile], profile, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_with_all_params(self, client: Kernel) -> None: + profile = client.profiles.list( + limit=1, + offset=0, + query="query", + ) + assert_matches_type(SyncOffsetPagination[Profile], profile, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -117,7 +128,7 @@ def test_raw_response_list(self, client: Kernel) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" profile = response.parse() - assert_matches_type(ProfileListResponse, profile, path=["response"]) + assert_matches_type(SyncOffsetPagination[Profile], profile, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -127,7 +138,7 @@ def test_streaming_response_list(self, client: Kernel) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" profile = response.parse() - assert_matches_type(ProfileListResponse, profile, path=["response"]) + assert_matches_type(SyncOffsetPagination[Profile], profile, path=["response"]) assert cast(Any, response.is_closed) is True @@ -311,7 +322,17 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: @parametrize async def test_method_list(self, async_client: AsyncKernel) -> None: profile = await async_client.profiles.list() - assert_matches_type(ProfileListResponse, profile, path=["response"]) + assert_matches_type(AsyncOffsetPagination[Profile], profile, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: + profile = await async_client.profiles.list( + limit=1, + offset=0, + query="query", + ) + assert_matches_type(AsyncOffsetPagination[Profile], profile, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -321,7 +342,7 @@ async def test_raw_response_list(self, async_client: AsyncKernel) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" profile = await response.parse() - assert_matches_type(ProfileListResponse, profile, path=["response"]) + assert_matches_type(AsyncOffsetPagination[Profile], profile, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -331,7 +352,7 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" profile = await response.parse() - assert_matches_type(ProfileListResponse, profile, path=["response"]) + assert_matches_type(AsyncOffsetPagination[Profile], profile, path=["response"]) assert cast(Any, response.is_closed) is True From 26b0d2dc772223b790c667a51a0387a199ce1b6c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 17:16:17 +0000 Subject: [PATCH 312/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 559b4513..51acdaa4 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.36.1" + ".": "0.37.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index f969dc23..b3ea5e98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.36.1" +version = "0.37.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index eca0c818..01a56b55 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.36.1" # x-release-please-version +__version__ = "0.37.0" # x-release-please-version From a57a36b11dccab2719717b69ad4ca4877a866989 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 03:31:00 +0000 Subject: [PATCH 313/448] chore(internal): add request options to SSE classes --- src/kernel/_response.py | 3 +++ src/kernel/_streaming.py | 11 ++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/kernel/_response.py b/src/kernel/_response.py index 89c72c39..a6c5fa30 100644 --- a/src/kernel/_response.py +++ b/src/kernel/_response.py @@ -152,6 +152,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: ), response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) @@ -162,6 +163,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: cast_to=extract_stream_chunk_type(self._stream_cls), response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) @@ -175,6 +177,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: cast_to=cast_to, response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) diff --git a/src/kernel/_streaming.py b/src/kernel/_streaming.py index 369a3f6d..5520edb5 100644 --- a/src/kernel/_streaming.py +++ b/src/kernel/_streaming.py @@ -4,7 +4,7 @@ import json import inspect from types import TracebackType -from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, AsyncIterator, cast +from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, Optional, AsyncIterator, cast from typing_extensions import Self, Protocol, TypeGuard, override, get_origin, runtime_checkable import httpx @@ -13,6 +13,7 @@ if TYPE_CHECKING: from ._client import Kernel, AsyncKernel + from ._models import FinalRequestOptions _T = TypeVar("_T") @@ -22,7 +23,7 @@ class Stream(Generic[_T]): """Provides the core interface to iterate over a synchronous stream response.""" response: httpx.Response - + _options: Optional[FinalRequestOptions] = None _decoder: SSEBytesDecoder def __init__( @@ -31,10 +32,12 @@ def __init__( cast_to: type[_T], response: httpx.Response, client: Kernel, + options: Optional[FinalRequestOptions] = None, ) -> None: self.response = response self._cast_to = cast_to self._client = client + self._options = options self._decoder = client._make_sse_decoder() self._iterator = self.__stream__() @@ -85,7 +88,7 @@ class AsyncStream(Generic[_T]): """Provides the core interface to iterate over an asynchronous stream response.""" response: httpx.Response - + _options: Optional[FinalRequestOptions] = None _decoder: SSEDecoder | SSEBytesDecoder def __init__( @@ -94,10 +97,12 @@ def __init__( cast_to: type[_T], response: httpx.Response, client: AsyncKernel, + options: Optional[FinalRequestOptions] = None, ) -> None: self.response = response self._cast_to = cast_to self._client = client + self._options = options self._decoder = client._make_sse_decoder() self._iterator = self.__stream__() From d7c3b435c30bb7adfd40e2f6876ab9a459b5960f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 03:39:55 +0000 Subject: [PATCH 314/448] chore(internal): make `test_proxy_environment_variables` more resilient --- tests/test_client.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index 4d3fb09a..8541457d 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -955,6 +955,8 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + # Delete in case our environment has this set + monkeypatch.delenv("HTTP_PROXY", raising=False) client = DefaultHttpxClient() @@ -1869,6 +1871,8 @@ async def test_get_platform(self) -> None: async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + # Delete in case our environment has this set + monkeypatch.delenv("HTTP_PROXY", raising=False) client = DefaultAsyncHttpxClient() From e59bb14dd04db9069306e0688191765ae2cfdd93 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 03:21:43 +0000 Subject: [PATCH 315/448] chore(internal): make `test_proxy_environment_variables` more resilient to env --- tests/test_client.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 8541457d..e5f4f849 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -955,8 +955,14 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly monkeypatch.setenv("HTTPS_PROXY", "https://example.org") - # Delete in case our environment has this set + # Delete in case our environment has any proxy env vars set monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.delenv("NO_PROXY", raising=False) + monkeypatch.delenv("http_proxy", raising=False) + monkeypatch.delenv("https_proxy", raising=False) + monkeypatch.delenv("all_proxy", raising=False) + monkeypatch.delenv("no_proxy", raising=False) client = DefaultHttpxClient() @@ -1871,8 +1877,14 @@ async def test_get_platform(self) -> None: async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly monkeypatch.setenv("HTTPS_PROXY", "https://example.org") - # Delete in case our environment has this set + # Delete in case our environment has any proxy env vars set monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.delenv("NO_PROXY", raising=False) + monkeypatch.delenv("http_proxy", raising=False) + monkeypatch.delenv("https_proxy", raising=False) + monkeypatch.delenv("all_proxy", raising=False) + monkeypatch.delenv("no_proxy", raising=False) client = DefaultAsyncHttpxClient() From fd5e21af112477d6fd98bb4d0c160211da1b9bf4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 18:23:03 +0000 Subject: [PATCH 316/448] feat: Neil/kernel 1029 past session search --- .stats.yml | 6 +++--- src/kernel/resources/browsers/browsers.py | 8 ++++++++ src/kernel/types/browser_list_params.py | 3 +++ tests/api_resources/test_browsers.py | 2 ++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index b154e7ae..3e385c42 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 101 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-fc4a441d80d9a26574ef8af390a0c76265f5d4190daf90a04b6b353b128bbd97.yml -openapi_spec_hash: 192987649d3797c3a80e6ef201667b64 -config_hash: 8af430e19f4af86c05f2987241cae72f +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-9462b3d8f055f8bda06da65583f5aa09a17d35254c5983796d8e84ebb3c62c47.yml +openapi_spec_hash: 1914dd35b8e0e5a21ccec91eac2a616d +config_hash: c6b88eea9a15840f26130eb8ed3b42a0 diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 58c3e2a3..59cecda1 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -329,6 +329,7 @@ def list( include_deleted: bool | Omit = omit, limit: int | Omit = omit, offset: int | Omit = omit, + query: str | Omit = omit, status: Literal["active", "deleted", "all"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -350,6 +351,8 @@ def list( offset: Number of results to skip. Defaults to 0. + query: Search browsers by session ID, profile ID, or proxy ID. + status: Filter sessions by status. "active" returns only active sessions (default), "deleted" returns only soft-deleted sessions, "all" returns both. @@ -374,6 +377,7 @@ def list( "include_deleted": include_deleted, "limit": limit, "offset": offset, + "query": query, "status": status, }, browser_list_params.BrowserListParams, @@ -745,6 +749,7 @@ def list( include_deleted: bool | Omit = omit, limit: int | Omit = omit, offset: int | Omit = omit, + query: str | Omit = omit, status: Literal["active", "deleted", "all"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -766,6 +771,8 @@ def list( offset: Number of results to skip. Defaults to 0. + query: Search browsers by session ID, profile ID, or proxy ID. + status: Filter sessions by status. "active" returns only active sessions (default), "deleted" returns only soft-deleted sessions, "all" returns both. @@ -790,6 +797,7 @@ def list( "include_deleted": include_deleted, "limit": limit, "offset": offset, + "query": query, "status": status, }, browser_list_params.BrowserListParams, diff --git a/src/kernel/types/browser_list_params.py b/src/kernel/types/browser_list_params.py index 02aa97a6..4c858e1c 100644 --- a/src/kernel/types/browser_list_params.py +++ b/src/kernel/types/browser_list_params.py @@ -21,6 +21,9 @@ class BrowserListParams(TypedDict, total=False): offset: int """Number of results to skip. Defaults to 0.""" + query: str + """Search browsers by session ID, profile ID, or proxy ID.""" + status: Literal["active", "deleted", "all"] """Filter sessions by status. diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index c0319df9..2addbb87 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -209,6 +209,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None: include_deleted=True, limit=1, offset=0, + query="query", status="active", ) assert_matches_type(SyncOffsetPagination[BrowserListResponse], browser, path=["response"]) @@ -571,6 +572,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N include_deleted=True, limit=1, offset=0, + query="query", status="active", ) assert_matches_type(AsyncOffsetPagination[BrowserListResponse], browser, path=["response"]) From 933c7f45161b5915fa4ec581f7f26509d11cd5b7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 18:54:57 +0000 Subject: [PATCH 317/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 51acdaa4..8ea07c9a 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.37.0" + ".": "0.38.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index b3ea5e98..09fdb6b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.37.0" +version = "0.38.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 01a56b55..c364cf29 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.37.0" # x-release-please-version +__version__ = "0.38.0" # x-release-please-version From 57d9b73e9c540a058616da84ba0fc3b107b1918c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 22:42:51 +0000 Subject: [PATCH 318/448] feat: Add proxy hostname bypass hosts --- .stats.yml | 4 ++-- src/kernel/resources/proxies.py | 10 +++++++++- src/kernel/types/proxy_check_response.py | 5 ++++- src/kernel/types/proxy_create_params.py | 5 +++++ src/kernel/types/proxy_create_response.py | 5 ++++- src/kernel/types/proxy_list_response.py | 3 +++ src/kernel/types/proxy_retrieve_response.py | 5 ++++- tests/api_resources/test_proxies.py | 2 ++ 8 files changed, 33 insertions(+), 6 deletions(-) diff --git a/.stats.yml b/.stats.yml index 3e385c42..9538870c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 101 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-9462b3d8f055f8bda06da65583f5aa09a17d35254c5983796d8e84ebb3c62c47.yml -openapi_spec_hash: 1914dd35b8e0e5a21ccec91eac2a616d +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-d53de581fcac5c3b06940fc93667b9cd2a6a60dd3674da7c1f47484b0f442bf8.yml +openapi_spec_hash: 177d0c537b7e5357c815bb64175e6484 config_hash: c6b88eea9a15840f26130eb8ed3b42a0 diff --git a/src/kernel/resources/proxies.py b/src/kernel/resources/proxies.py index 6574a256..d42f7b04 100644 --- a/src/kernel/resources/proxies.py +++ b/src/kernel/resources/proxies.py @@ -7,7 +7,7 @@ import httpx from ..types import proxy_create_params -from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given +from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, SequenceNotStr, omit, not_given from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -50,6 +50,7 @@ def create( self, *, type: Literal["datacenter", "isp", "residential", "mobile", "custom"], + bypass_hosts: SequenceNotStr[str] | Omit = omit, config: proxy_create_params.Config | Omit = omit, name: str | Omit = omit, protocol: Literal["http", "https"] | Omit = omit, @@ -67,6 +68,8 @@ def create( type: Proxy type to use. In terms of quality for avoiding bot-detection, from best to worst: `mobile` > `residential` > `isp` > `datacenter`. + bypass_hosts: Hostnames that should bypass the parent proxy and connect directly. + config: Configuration specific to the selected proxy `type`. name: Readable name of the proxy. @@ -86,6 +89,7 @@ def create( body=maybe_transform( { "type": type, + "bypass_hosts": bypass_hosts, "config": config, "name": name, "protocol": protocol, @@ -243,6 +247,7 @@ async def create( self, *, type: Literal["datacenter", "isp", "residential", "mobile", "custom"], + bypass_hosts: SequenceNotStr[str] | Omit = omit, config: proxy_create_params.Config | Omit = omit, name: str | Omit = omit, protocol: Literal["http", "https"] | Omit = omit, @@ -260,6 +265,8 @@ async def create( type: Proxy type to use. In terms of quality for avoiding bot-detection, from best to worst: `mobile` > `residential` > `isp` > `datacenter`. + bypass_hosts: Hostnames that should bypass the parent proxy and connect directly. + config: Configuration specific to the selected proxy `type`. name: Readable name of the proxy. @@ -279,6 +286,7 @@ async def create( body=await async_maybe_transform( { "type": type, + "bypass_hosts": bypass_hosts, "config": config, "name": name, "protocol": protocol, diff --git a/src/kernel/types/proxy_check_response.py b/src/kernel/types/proxy_check_response.py index 26b6d0a1..c26d665d 100644 --- a/src/kernel/types/proxy_check_response.py +++ b/src/kernel/types/proxy_check_response.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Union, Optional +from typing import List, Union, Optional from datetime import datetime from typing_extensions import Literal, TypeAlias @@ -179,6 +179,9 @@ class ProxyCheckResponse(BaseModel): id: Optional[str] = None + bypass_hosts: Optional[List[str]] = None + """Hostnames that should bypass the parent proxy and connect directly.""" + config: Optional[Config] = None """Configuration specific to the selected proxy `type`.""" diff --git a/src/kernel/types/proxy_create_params.py b/src/kernel/types/proxy_create_params.py index 0a3536f0..175b95ff 100644 --- a/src/kernel/types/proxy_create_params.py +++ b/src/kernel/types/proxy_create_params.py @@ -5,6 +5,8 @@ from typing import Union from typing_extensions import Literal, Required, TypeAlias, TypedDict +from .._types import SequenceNotStr + __all__ = [ "ProxyCreateParams", "Config", @@ -24,6 +26,9 @@ class ProxyCreateParams(TypedDict, total=False): `residential` > `isp` > `datacenter`. """ + bypass_hosts: SequenceNotStr[str] + """Hostnames that should bypass the parent proxy and connect directly.""" + config: Config """Configuration specific to the selected proxy `type`.""" diff --git a/src/kernel/types/proxy_create_response.py b/src/kernel/types/proxy_create_response.py index 939ec4f7..d317662f 100644 --- a/src/kernel/types/proxy_create_response.py +++ b/src/kernel/types/proxy_create_response.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Union, Optional +from typing import List, Union, Optional from datetime import datetime from typing_extensions import Literal, TypeAlias @@ -179,6 +179,9 @@ class ProxyCreateResponse(BaseModel): id: Optional[str] = None + bypass_hosts: Optional[List[str]] = None + """Hostnames that should bypass the parent proxy and connect directly.""" + config: Optional[Config] = None """Configuration specific to the selected proxy `type`.""" diff --git a/src/kernel/types/proxy_list_response.py b/src/kernel/types/proxy_list_response.py index 2d1ffb99..bbbe17c7 100644 --- a/src/kernel/types/proxy_list_response.py +++ b/src/kernel/types/proxy_list_response.py @@ -180,6 +180,9 @@ class ProxyListResponseItem(BaseModel): id: Optional[str] = None + bypass_hosts: Optional[List[str]] = None + """Hostnames that should bypass the parent proxy and connect directly.""" + config: Optional[ProxyListResponseItemConfig] = None """Configuration specific to the selected proxy `type`.""" diff --git a/src/kernel/types/proxy_retrieve_response.py b/src/kernel/types/proxy_retrieve_response.py index bf99ed0e..6b0b1bbe 100644 --- a/src/kernel/types/proxy_retrieve_response.py +++ b/src/kernel/types/proxy_retrieve_response.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Union, Optional +from typing import List, Union, Optional from datetime import datetime from typing_extensions import Literal, TypeAlias @@ -179,6 +179,9 @@ class ProxyRetrieveResponse(BaseModel): id: Optional[str] = None + bypass_hosts: Optional[List[str]] = None + """Hostnames that should bypass the parent proxy and connect directly.""" + config: Optional[Config] = None """Configuration specific to the selected proxy `type`.""" diff --git a/tests/api_resources/test_proxies.py b/tests/api_resources/test_proxies.py index 3caabece..9f107d2b 100644 --- a/tests/api_resources/test_proxies.py +++ b/tests/api_resources/test_proxies.py @@ -35,6 +35,7 @@ def test_method_create(self, client: Kernel) -> None: def test_method_create_with_all_params(self, client: Kernel) -> None: proxy = client.proxies.create( type="datacenter", + bypass_hosts=["string"], config={"country": "US"}, name="name", protocol="http", @@ -240,6 +241,7 @@ async def test_method_create(self, async_client: AsyncKernel) -> None: async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: proxy = await async_client.proxies.create( type="datacenter", + bypass_hosts=["string"], config={"country": "US"}, name="name", protocol="http", From 60fdd9b9bb20c39c31a07cf3e3dcbf9ad4fc179e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 22:45:54 +0000 Subject: [PATCH 319/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 8ea07c9a..1b5dc400 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.38.0" + ".": "0.39.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 09fdb6b1..3db9ef1f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.38.0" +version = "0.39.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index c364cf29..adb66af7 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.38.0" # x-release-please-version +__version__ = "0.39.0" # x-release-please-version From ab9377d9703b23c095513ae55c9dfd4432ade7f6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 21:22:15 +0000 Subject: [PATCH 320/448] feat: show pool browsers in dashboard and API --- .stats.yml | 6 +++--- api.md | 1 + src/kernel/types/__init__.py | 1 + src/kernel/types/browser_create_response.py | 4 ++++ src/kernel/types/browser_list_response.py | 4 ++++ .../types/browser_pool_acquire_response.py | 4 ++++ src/kernel/types/browser_pool_ref.py | 17 +++++++++++++++++ src/kernel/types/browser_retrieve_response.py | 4 ++++ src/kernel/types/browser_update_response.py | 4 ++++ .../types/invocation_list_browsers_response.py | 4 ++++ 10 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 src/kernel/types/browser_pool_ref.py diff --git a/.stats.yml b/.stats.yml index 9538870c..49cb307e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 101 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-d53de581fcac5c3b06940fc93667b9cd2a6a60dd3674da7c1f47484b0f442bf8.yml -openapi_spec_hash: 177d0c537b7e5357c815bb64175e6484 -config_hash: c6b88eea9a15840f26130eb8ed3b42a0 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-e6e88da6e6fffe12873a108ca33ebfbd59b85232078ab0e4dca5c8273c131053.yml +openapi_spec_hash: 4f22b8ec1d048cc74a751e3ab39b943c +config_hash: 6bac5bbe5d5fc26e0912e33f646adb14 diff --git a/api.md b/api.md index 0cda6cac..2a46da7b 100644 --- a/api.md +++ b/api.md @@ -81,6 +81,7 @@ Types: ```python from kernel.types import ( BrowserPersistence, + BrowserPoolRef, Profile, BrowserCreateResponse, BrowserRetrieveResponse, diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 2c3b4a8f..f81c1a47 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -17,6 +17,7 @@ from .credential import Credential as Credential from .browser_pool import BrowserPool as BrowserPool from .app_list_params import AppListParams as AppListParams +from .browser_pool_ref import BrowserPoolRef as BrowserPoolRef from .app_list_response import AppListResponse as AppListResponse from .browser_list_params import BrowserListParams as BrowserListParams from .browser_persistence import BrowserPersistence as BrowserPersistence diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index 051d739f..86028555 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -5,6 +5,7 @@ from .profile import Profile from .._models import BaseModel +from .browser_pool_ref import BrowserPoolRef from .browser_persistence import BrowserPersistence from .shared.browser_viewport import BrowserViewport @@ -48,6 +49,9 @@ class BrowserCreateResponse(BaseModel): persistence: Optional[BrowserPersistence] = None """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead.""" + pool: Optional[BrowserPoolRef] = None + """Browser pool this session was acquired from, if any.""" + profile: Optional[Profile] = None """Browser profile metadata.""" diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index 85d1dd1a..00445c7e 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -5,6 +5,7 @@ from .profile import Profile from .._models import BaseModel +from .browser_pool_ref import BrowserPoolRef from .browser_persistence import BrowserPersistence from .shared.browser_viewport import BrowserViewport @@ -48,6 +49,9 @@ class BrowserListResponse(BaseModel): persistence: Optional[BrowserPersistence] = None """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead.""" + pool: Optional[BrowserPoolRef] = None + """Browser pool this session was acquired from, if any.""" + profile: Optional[Profile] = None """Browser profile metadata.""" diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py index b8d066d8..c237b3ab 100644 --- a/src/kernel/types/browser_pool_acquire_response.py +++ b/src/kernel/types/browser_pool_acquire_response.py @@ -5,6 +5,7 @@ from .profile import Profile from .._models import BaseModel +from .browser_pool_ref import BrowserPoolRef from .browser_persistence import BrowserPersistence from .shared.browser_viewport import BrowserViewport @@ -48,6 +49,9 @@ class BrowserPoolAcquireResponse(BaseModel): persistence: Optional[BrowserPersistence] = None """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead.""" + pool: Optional[BrowserPoolRef] = None + """Browser pool this session was acquired from, if any.""" + profile: Optional[Profile] = None """Browser profile metadata.""" diff --git a/src/kernel/types/browser_pool_ref.py b/src/kernel/types/browser_pool_ref.py new file mode 100644 index 00000000..326ad7b7 --- /dev/null +++ b/src/kernel/types/browser_pool_ref.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["BrowserPoolRef"] + + +class BrowserPoolRef(BaseModel): + """Browser pool this session was acquired from, if any.""" + + id: str + """Browser pool ID""" + + name: Optional[str] = None + """Browser pool name, if set""" diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index ee99dcd5..9c4f70c1 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -5,6 +5,7 @@ from .profile import Profile from .._models import BaseModel +from .browser_pool_ref import BrowserPoolRef from .browser_persistence import BrowserPersistence from .shared.browser_viewport import BrowserViewport @@ -48,6 +49,9 @@ class BrowserRetrieveResponse(BaseModel): persistence: Optional[BrowserPersistence] = None """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead.""" + pool: Optional[BrowserPoolRef] = None + """Browser pool this session was acquired from, if any.""" + profile: Optional[Profile] = None """Browser profile metadata.""" diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py index 6591144e..912fdaee 100644 --- a/src/kernel/types/browser_update_response.py +++ b/src/kernel/types/browser_update_response.py @@ -5,6 +5,7 @@ from .profile import Profile from .._models import BaseModel +from .browser_pool_ref import BrowserPoolRef from .browser_persistence import BrowserPersistence from .shared.browser_viewport import BrowserViewport @@ -48,6 +49,9 @@ class BrowserUpdateResponse(BaseModel): persistence: Optional[BrowserPersistence] = None """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead.""" + pool: Optional[BrowserPoolRef] = None + """Browser pool this session was acquired from, if any.""" + profile: Optional[Profile] = None """Browser profile metadata.""" diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py index a1b1a08b..6988ff3f 100644 --- a/src/kernel/types/invocation_list_browsers_response.py +++ b/src/kernel/types/invocation_list_browsers_response.py @@ -5,6 +5,7 @@ from .profile import Profile from .._models import BaseModel +from .browser_pool_ref import BrowserPoolRef from .browser_persistence import BrowserPersistence from .shared.browser_viewport import BrowserViewport @@ -48,6 +49,9 @@ class Browser(BaseModel): persistence: Optional[BrowserPersistence] = None """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead.""" + pool: Optional[BrowserPoolRef] = None + """Browser pool this session was acquired from, if any.""" + profile: Optional[Profile] = None """Browser profile metadata.""" From d89e4d298ba763dda95bcbbf98677033ea07c7ad Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 21:29:55 +0000 Subject: [PATCH 321/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 1b5dc400..0a40b9d7 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.39.0" + ".": "0.40.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 3db9ef1f..d0350784 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.39.0" +version = "0.40.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index adb66af7..6983874b 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.39.0" # x-release-please-version +__version__ = "0.40.0" # x-release-please-version From 2f8f63a0a26ec748e9e9539af0b77a5880e31f85 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 00:47:08 +0000 Subject: [PATCH 322/448] feat: Return uptime_ms for deleted browser sessions --- .stats.yml | 6 +++--- api.md | 1 + src/kernel/types/__init__.py | 1 + src/kernel/types/browser_create_response.py | 4 ++++ src/kernel/types/browser_list_response.py | 4 ++++ src/kernel/types/browser_pool_acquire_response.py | 4 ++++ src/kernel/types/browser_retrieve_response.py | 4 ++++ src/kernel/types/browser_update_response.py | 4 ++++ src/kernel/types/browser_usage.py | 12 ++++++++++++ .../types/invocation_list_browsers_response.py | 4 ++++ 10 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 src/kernel/types/browser_usage.py diff --git a/.stats.yml b/.stats.yml index 49cb307e..4e3164c9 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 101 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-e6e88da6e6fffe12873a108ca33ebfbd59b85232078ab0e4dca5c8273c131053.yml -openapi_spec_hash: 4f22b8ec1d048cc74a751e3ab39b943c -config_hash: 6bac5bbe5d5fc26e0912e33f646adb14 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-586ddc36cd621b3705138de66a0e7d28d1c1485064aa85ce09ce24edb50003ef.yml +openapi_spec_hash: 8e8d4bd31e4920303e7ec9ce313fb1ec +config_hash: 81f143f4bee47ae7b0b8357551babadf diff --git a/api.md b/api.md index 2a46da7b..511ab51f 100644 --- a/api.md +++ b/api.md @@ -82,6 +82,7 @@ Types: from kernel.types import ( BrowserPersistence, BrowserPoolRef, + BrowserUsage, Profile, BrowserCreateResponse, BrowserRetrieveResponse, diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index f81c1a47..894342ac 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -16,6 +16,7 @@ from .profile import Profile as Profile from .credential import Credential as Credential from .browser_pool import BrowserPool as BrowserPool +from .browser_usage import BrowserUsage as BrowserUsage from .app_list_params import AppListParams as AppListParams from .browser_pool_ref import BrowserPoolRef as BrowserPoolRef from .app_list_response import AppListResponse as AppListResponse diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index 86028555..646e25aa 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -5,6 +5,7 @@ from .profile import Profile from .._models import BaseModel +from .browser_usage import BrowserUsage from .browser_pool_ref import BrowserPoolRef from .browser_persistence import BrowserPersistence from .shared.browser_viewport import BrowserViewport @@ -58,6 +59,9 @@ class BrowserCreateResponse(BaseModel): proxy_id: Optional[str] = None """ID of the proxy associated with this browser session, if any.""" + usage: Optional[BrowserUsage] = None + """Session usage metrics.""" + viewport: Optional[BrowserViewport] = None """Initial browser window size in pixels with optional refresh rate. diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index 00445c7e..7bb748fb 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -5,6 +5,7 @@ from .profile import Profile from .._models import BaseModel +from .browser_usage import BrowserUsage from .browser_pool_ref import BrowserPoolRef from .browser_persistence import BrowserPersistence from .shared.browser_viewport import BrowserViewport @@ -58,6 +59,9 @@ class BrowserListResponse(BaseModel): proxy_id: Optional[str] = None """ID of the proxy associated with this browser session, if any.""" + usage: Optional[BrowserUsage] = None + """Session usage metrics.""" + viewport: Optional[BrowserViewport] = None """Initial browser window size in pixels with optional refresh rate. diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py index c237b3ab..3ffb7777 100644 --- a/src/kernel/types/browser_pool_acquire_response.py +++ b/src/kernel/types/browser_pool_acquire_response.py @@ -5,6 +5,7 @@ from .profile import Profile from .._models import BaseModel +from .browser_usage import BrowserUsage from .browser_pool_ref import BrowserPoolRef from .browser_persistence import BrowserPersistence from .shared.browser_viewport import BrowserViewport @@ -58,6 +59,9 @@ class BrowserPoolAcquireResponse(BaseModel): proxy_id: Optional[str] = None """ID of the proxy associated with this browser session, if any.""" + usage: Optional[BrowserUsage] = None + """Session usage metrics.""" + viewport: Optional[BrowserViewport] = None """Initial browser window size in pixels with optional refresh rate. diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 9c4f70c1..3a5df2ca 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -5,6 +5,7 @@ from .profile import Profile from .._models import BaseModel +from .browser_usage import BrowserUsage from .browser_pool_ref import BrowserPoolRef from .browser_persistence import BrowserPersistence from .shared.browser_viewport import BrowserViewport @@ -58,6 +59,9 @@ class BrowserRetrieveResponse(BaseModel): proxy_id: Optional[str] = None """ID of the proxy associated with this browser session, if any.""" + usage: Optional[BrowserUsage] = None + """Session usage metrics.""" + viewport: Optional[BrowserViewport] = None """Initial browser window size in pixels with optional refresh rate. diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py index 912fdaee..309a7f46 100644 --- a/src/kernel/types/browser_update_response.py +++ b/src/kernel/types/browser_update_response.py @@ -5,6 +5,7 @@ from .profile import Profile from .._models import BaseModel +from .browser_usage import BrowserUsage from .browser_pool_ref import BrowserPoolRef from .browser_persistence import BrowserPersistence from .shared.browser_viewport import BrowserViewport @@ -58,6 +59,9 @@ class BrowserUpdateResponse(BaseModel): proxy_id: Optional[str] = None """ID of the proxy associated with this browser session, if any.""" + usage: Optional[BrowserUsage] = None + """Session usage metrics.""" + viewport: Optional[BrowserViewport] = None """Initial browser window size in pixels with optional refresh rate. diff --git a/src/kernel/types/browser_usage.py b/src/kernel/types/browser_usage.py new file mode 100644 index 00000000..72bde9b6 --- /dev/null +++ b/src/kernel/types/browser_usage.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["BrowserUsage"] + + +class BrowserUsage(BaseModel): + """Session usage metrics.""" + + uptime_ms: int + """Time in milliseconds the session was actively running.""" diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py index 6988ff3f..cb98f332 100644 --- a/src/kernel/types/invocation_list_browsers_response.py +++ b/src/kernel/types/invocation_list_browsers_response.py @@ -5,6 +5,7 @@ from .profile import Profile from .._models import BaseModel +from .browser_usage import BrowserUsage from .browser_pool_ref import BrowserPoolRef from .browser_persistence import BrowserPersistence from .shared.browser_viewport import BrowserViewport @@ -58,6 +59,9 @@ class Browser(BaseModel): proxy_id: Optional[str] = None """ID of the proxy associated with this browser session, if any.""" + usage: Optional[BrowserUsage] = None + """Session usage metrics.""" + viewport: Optional[BrowserViewport] = None """Initial browser window size in pixels with optional refresh rate. From 606ce5aa9f8ef68fe33a7c9873d6f3d230c7d8b4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 01:03:08 +0000 Subject: [PATCH 323/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 0a40b9d7..ea2682c3 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.40.0" + ".": "0.41.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index d0350784..3d0824af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.40.0" +version = "0.41.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 6983874b..8859da90 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.40.0" # x-release-please-version +__version__ = "0.41.0" # x-release-please-version From 485f4bd241d912b3e9e266f7f17fd69639e2271f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 21:41:20 +0000 Subject: [PATCH 324/448] feat: Neil/kernel 1052 deployments list endpoint --- .stats.yml | 4 ++-- src/kernel/resources/apps.py | 8 ++++++++ src/kernel/types/app_list_params.py | 3 +++ tests/api_resources/test_apps.py | 2 ++ 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 4e3164c9..d4eead98 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 101 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-586ddc36cd621b3705138de66a0e7d28d1c1485064aa85ce09ce24edb50003ef.yml -openapi_spec_hash: 8e8d4bd31e4920303e7ec9ce313fb1ec +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-a935c8aae21f8ddb83ea5e289034df12cbde88d432fa2b287629814bb3f58bb6.yml +openapi_spec_hash: df3189b9728372f01662a19c060bcbc5 config_hash: 81f143f4bee47ae7b0b8357551babadf diff --git a/src/kernel/resources/apps.py b/src/kernel/resources/apps.py index 0443e73a..7383c29e 100644 --- a/src/kernel/resources/apps.py +++ b/src/kernel/resources/apps.py @@ -48,6 +48,7 @@ def list( app_name: str | Omit = omit, limit: int | Omit = omit, offset: int | Omit = omit, + query: str | Omit = omit, version: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -67,6 +68,8 @@ def list( offset: Offset the number of apps to return. + query: Search apps by name. + version: Filter results by version label. extra_headers: Send extra headers @@ -90,6 +93,7 @@ def list( "app_name": app_name, "limit": limit, "offset": offset, + "query": query, "version": version, }, app_list_params.AppListParams, @@ -125,6 +129,7 @@ def list( app_name: str | Omit = omit, limit: int | Omit = omit, offset: int | Omit = omit, + query: str | Omit = omit, version: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -144,6 +149,8 @@ def list( offset: Offset the number of apps to return. + query: Search apps by name. + version: Filter results by version label. extra_headers: Send extra headers @@ -167,6 +174,7 @@ def list( "app_name": app_name, "limit": limit, "offset": offset, + "query": query, "version": version, }, app_list_params.AppListParams, diff --git a/src/kernel/types/app_list_params.py b/src/kernel/types/app_list_params.py index 296ded55..a4a9db48 100644 --- a/src/kernel/types/app_list_params.py +++ b/src/kernel/types/app_list_params.py @@ -17,5 +17,8 @@ class AppListParams(TypedDict, total=False): offset: int """Offset the number of apps to return.""" + query: str + """Search apps by name.""" + version: str """Filter results by version label.""" diff --git a/tests/api_resources/test_apps.py b/tests/api_resources/test_apps.py index c8629fa0..cab677b3 100644 --- a/tests/api_resources/test_apps.py +++ b/tests/api_resources/test_apps.py @@ -31,6 +31,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None: app_name="app_name", limit=1, offset=0, + query="query", version="version", ) assert_matches_type(SyncOffsetPagination[AppListResponse], app, path=["response"]) @@ -76,6 +77,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N app_name="app_name", limit=1, offset=0, + query="query", version="version", ) assert_matches_type(AsyncOffsetPagination[AppListResponse], app, path=["response"]) From 061bb1e1af21d5f602925256926d54a49ca1821b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 21:50:32 +0000 Subject: [PATCH 325/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index ea2682c3..52afe059 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.41.0" + ".": "0.42.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 3d0824af..968ba8d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.41.0" +version = "0.42.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 8859da90..7a26ebd7 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.41.0" # x-release-please-version +__version__ = "0.42.0" # x-release-please-version From 847ba95ebab9f9bd5b4e025f1839027a48f8fe0b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 03:49:08 +0000 Subject: [PATCH 326/448] chore(internal): codegen related update --- src/kernel/_client.py | 60 ++++++++++++++++++++ src/kernel/resources/apps.py | 4 ++ src/kernel/resources/auth/auth.py | 6 ++ src/kernel/resources/auth/connections.py | 4 ++ src/kernel/resources/browser_pools.py | 4 ++ src/kernel/resources/browsers/browsers.py | 34 +++++++++++ src/kernel/resources/browsers/fs/fs.py | 10 ++++ src/kernel/resources/browsers/fs/watch.py | 4 ++ src/kernel/resources/browsers/logs.py | 4 ++ src/kernel/resources/browsers/playwright.py | 4 ++ src/kernel/resources/browsers/process.py | 4 ++ src/kernel/resources/browsers/replays.py | 4 ++ src/kernel/resources/credential_providers.py | 4 ++ src/kernel/resources/credentials.py | 4 ++ src/kernel/resources/deployments.py | 4 ++ src/kernel/resources/extensions.py | 4 ++ src/kernel/resources/invocations.py | 4 ++ src/kernel/resources/profiles.py | 4 ++ src/kernel/resources/proxies.py | 4 ++ 19 files changed, 170 insertions(+) diff --git a/src/kernel/_client.py b/src/kernel/_client.py index df7fd255..84fc48dd 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -155,30 +155,35 @@ def __init__( @cached_property def deployments(self) -> DeploymentsResource: + """Create and manage app deployments and stream deployment events.""" from .resources.deployments import DeploymentsResource return DeploymentsResource(self) @cached_property def apps(self) -> AppsResource: + """List applications and versions.""" from .resources.apps import AppsResource return AppsResource(self) @cached_property def invocations(self) -> InvocationsResource: + """Invoke actions and stream or query invocation status and events.""" from .resources.invocations import InvocationsResource return InvocationsResource(self) @cached_property def browsers(self) -> BrowsersResource: + """Create and manage browser sessions.""" from .resources.browsers import BrowsersResource return BrowsersResource(self) @cached_property def profiles(self) -> ProfilesResource: + """Create, list, retrieve, and delete browser profiles.""" from .resources.profiles import ProfilesResource return ProfilesResource(self) @@ -191,30 +196,35 @@ def auth(self) -> AuthResource: @cached_property def proxies(self) -> ProxiesResource: + """Create and manage proxy configurations for routing browser traffic.""" from .resources.proxies import ProxiesResource return ProxiesResource(self) @cached_property def extensions(self) -> ExtensionsResource: + """Create, list, retrieve, and delete browser extensions.""" from .resources.extensions import ExtensionsResource return ExtensionsResource(self) @cached_property def browser_pools(self) -> BrowserPoolsResource: + """Create and manage browser pools for acquiring and releasing browsers.""" from .resources.browser_pools import BrowserPoolsResource return BrowserPoolsResource(self) @cached_property def credentials(self) -> CredentialsResource: + """Create and manage credentials for authentication.""" from .resources.credentials import CredentialsResource return CredentialsResource(self) @cached_property def credential_providers(self) -> CredentialProvidersResource: + """Configure external credential providers like 1Password.""" from .resources.credential_providers import CredentialProvidersResource return CredentialProvidersResource(self) @@ -415,30 +425,35 @@ def __init__( @cached_property def deployments(self) -> AsyncDeploymentsResource: + """Create and manage app deployments and stream deployment events.""" from .resources.deployments import AsyncDeploymentsResource return AsyncDeploymentsResource(self) @cached_property def apps(self) -> AsyncAppsResource: + """List applications and versions.""" from .resources.apps import AsyncAppsResource return AsyncAppsResource(self) @cached_property def invocations(self) -> AsyncInvocationsResource: + """Invoke actions and stream or query invocation status and events.""" from .resources.invocations import AsyncInvocationsResource return AsyncInvocationsResource(self) @cached_property def browsers(self) -> AsyncBrowsersResource: + """Create and manage browser sessions.""" from .resources.browsers import AsyncBrowsersResource return AsyncBrowsersResource(self) @cached_property def profiles(self) -> AsyncProfilesResource: + """Create, list, retrieve, and delete browser profiles.""" from .resources.profiles import AsyncProfilesResource return AsyncProfilesResource(self) @@ -451,30 +466,35 @@ def auth(self) -> AsyncAuthResource: @cached_property def proxies(self) -> AsyncProxiesResource: + """Create and manage proxy configurations for routing browser traffic.""" from .resources.proxies import AsyncProxiesResource return AsyncProxiesResource(self) @cached_property def extensions(self) -> AsyncExtensionsResource: + """Create, list, retrieve, and delete browser extensions.""" from .resources.extensions import AsyncExtensionsResource return AsyncExtensionsResource(self) @cached_property def browser_pools(self) -> AsyncBrowserPoolsResource: + """Create and manage browser pools for acquiring and releasing browsers.""" from .resources.browser_pools import AsyncBrowserPoolsResource return AsyncBrowserPoolsResource(self) @cached_property def credentials(self) -> AsyncCredentialsResource: + """Create and manage credentials for authentication.""" from .resources.credentials import AsyncCredentialsResource return AsyncCredentialsResource(self) @cached_property def credential_providers(self) -> AsyncCredentialProvidersResource: + """Configure external credential providers like 1Password.""" from .resources.credential_providers import AsyncCredentialProvidersResource return AsyncCredentialProvidersResource(self) @@ -602,30 +622,35 @@ def __init__(self, client: Kernel) -> None: @cached_property def deployments(self) -> deployments.DeploymentsResourceWithRawResponse: + """Create and manage app deployments and stream deployment events.""" from .resources.deployments import DeploymentsResourceWithRawResponse return DeploymentsResourceWithRawResponse(self._client.deployments) @cached_property def apps(self) -> apps.AppsResourceWithRawResponse: + """List applications and versions.""" from .resources.apps import AppsResourceWithRawResponse return AppsResourceWithRawResponse(self._client.apps) @cached_property def invocations(self) -> invocations.InvocationsResourceWithRawResponse: + """Invoke actions and stream or query invocation status and events.""" from .resources.invocations import InvocationsResourceWithRawResponse return InvocationsResourceWithRawResponse(self._client.invocations) @cached_property def browsers(self) -> browsers.BrowsersResourceWithRawResponse: + """Create and manage browser sessions.""" from .resources.browsers import BrowsersResourceWithRawResponse return BrowsersResourceWithRawResponse(self._client.browsers) @cached_property def profiles(self) -> profiles.ProfilesResourceWithRawResponse: + """Create, list, retrieve, and delete browser profiles.""" from .resources.profiles import ProfilesResourceWithRawResponse return ProfilesResourceWithRawResponse(self._client.profiles) @@ -638,30 +663,35 @@ def auth(self) -> auth.AuthResourceWithRawResponse: @cached_property def proxies(self) -> proxies.ProxiesResourceWithRawResponse: + """Create and manage proxy configurations for routing browser traffic.""" from .resources.proxies import ProxiesResourceWithRawResponse return ProxiesResourceWithRawResponse(self._client.proxies) @cached_property def extensions(self) -> extensions.ExtensionsResourceWithRawResponse: + """Create, list, retrieve, and delete browser extensions.""" from .resources.extensions import ExtensionsResourceWithRawResponse return ExtensionsResourceWithRawResponse(self._client.extensions) @cached_property def browser_pools(self) -> browser_pools.BrowserPoolsResourceWithRawResponse: + """Create and manage browser pools for acquiring and releasing browsers.""" from .resources.browser_pools import BrowserPoolsResourceWithRawResponse return BrowserPoolsResourceWithRawResponse(self._client.browser_pools) @cached_property def credentials(self) -> credentials.CredentialsResourceWithRawResponse: + """Create and manage credentials for authentication.""" from .resources.credentials import CredentialsResourceWithRawResponse return CredentialsResourceWithRawResponse(self._client.credentials) @cached_property def credential_providers(self) -> credential_providers.CredentialProvidersResourceWithRawResponse: + """Configure external credential providers like 1Password.""" from .resources.credential_providers import CredentialProvidersResourceWithRawResponse return CredentialProvidersResourceWithRawResponse(self._client.credential_providers) @@ -675,30 +705,35 @@ def __init__(self, client: AsyncKernel) -> None: @cached_property def deployments(self) -> deployments.AsyncDeploymentsResourceWithRawResponse: + """Create and manage app deployments and stream deployment events.""" from .resources.deployments import AsyncDeploymentsResourceWithRawResponse return AsyncDeploymentsResourceWithRawResponse(self._client.deployments) @cached_property def apps(self) -> apps.AsyncAppsResourceWithRawResponse: + """List applications and versions.""" from .resources.apps import AsyncAppsResourceWithRawResponse return AsyncAppsResourceWithRawResponse(self._client.apps) @cached_property def invocations(self) -> invocations.AsyncInvocationsResourceWithRawResponse: + """Invoke actions and stream or query invocation status and events.""" from .resources.invocations import AsyncInvocationsResourceWithRawResponse return AsyncInvocationsResourceWithRawResponse(self._client.invocations) @cached_property def browsers(self) -> browsers.AsyncBrowsersResourceWithRawResponse: + """Create and manage browser sessions.""" from .resources.browsers import AsyncBrowsersResourceWithRawResponse return AsyncBrowsersResourceWithRawResponse(self._client.browsers) @cached_property def profiles(self) -> profiles.AsyncProfilesResourceWithRawResponse: + """Create, list, retrieve, and delete browser profiles.""" from .resources.profiles import AsyncProfilesResourceWithRawResponse return AsyncProfilesResourceWithRawResponse(self._client.profiles) @@ -711,30 +746,35 @@ def auth(self) -> auth.AsyncAuthResourceWithRawResponse: @cached_property def proxies(self) -> proxies.AsyncProxiesResourceWithRawResponse: + """Create and manage proxy configurations for routing browser traffic.""" from .resources.proxies import AsyncProxiesResourceWithRawResponse return AsyncProxiesResourceWithRawResponse(self._client.proxies) @cached_property def extensions(self) -> extensions.AsyncExtensionsResourceWithRawResponse: + """Create, list, retrieve, and delete browser extensions.""" from .resources.extensions import AsyncExtensionsResourceWithRawResponse return AsyncExtensionsResourceWithRawResponse(self._client.extensions) @cached_property def browser_pools(self) -> browser_pools.AsyncBrowserPoolsResourceWithRawResponse: + """Create and manage browser pools for acquiring and releasing browsers.""" from .resources.browser_pools import AsyncBrowserPoolsResourceWithRawResponse return AsyncBrowserPoolsResourceWithRawResponse(self._client.browser_pools) @cached_property def credentials(self) -> credentials.AsyncCredentialsResourceWithRawResponse: + """Create and manage credentials for authentication.""" from .resources.credentials import AsyncCredentialsResourceWithRawResponse return AsyncCredentialsResourceWithRawResponse(self._client.credentials) @cached_property def credential_providers(self) -> credential_providers.AsyncCredentialProvidersResourceWithRawResponse: + """Configure external credential providers like 1Password.""" from .resources.credential_providers import AsyncCredentialProvidersResourceWithRawResponse return AsyncCredentialProvidersResourceWithRawResponse(self._client.credential_providers) @@ -748,30 +788,35 @@ def __init__(self, client: Kernel) -> None: @cached_property def deployments(self) -> deployments.DeploymentsResourceWithStreamingResponse: + """Create and manage app deployments and stream deployment events.""" from .resources.deployments import DeploymentsResourceWithStreamingResponse return DeploymentsResourceWithStreamingResponse(self._client.deployments) @cached_property def apps(self) -> apps.AppsResourceWithStreamingResponse: + """List applications and versions.""" from .resources.apps import AppsResourceWithStreamingResponse return AppsResourceWithStreamingResponse(self._client.apps) @cached_property def invocations(self) -> invocations.InvocationsResourceWithStreamingResponse: + """Invoke actions and stream or query invocation status and events.""" from .resources.invocations import InvocationsResourceWithStreamingResponse return InvocationsResourceWithStreamingResponse(self._client.invocations) @cached_property def browsers(self) -> browsers.BrowsersResourceWithStreamingResponse: + """Create and manage browser sessions.""" from .resources.browsers import BrowsersResourceWithStreamingResponse return BrowsersResourceWithStreamingResponse(self._client.browsers) @cached_property def profiles(self) -> profiles.ProfilesResourceWithStreamingResponse: + """Create, list, retrieve, and delete browser profiles.""" from .resources.profiles import ProfilesResourceWithStreamingResponse return ProfilesResourceWithStreamingResponse(self._client.profiles) @@ -784,30 +829,35 @@ def auth(self) -> auth.AuthResourceWithStreamingResponse: @cached_property def proxies(self) -> proxies.ProxiesResourceWithStreamingResponse: + """Create and manage proxy configurations for routing browser traffic.""" from .resources.proxies import ProxiesResourceWithStreamingResponse return ProxiesResourceWithStreamingResponse(self._client.proxies) @cached_property def extensions(self) -> extensions.ExtensionsResourceWithStreamingResponse: + """Create, list, retrieve, and delete browser extensions.""" from .resources.extensions import ExtensionsResourceWithStreamingResponse return ExtensionsResourceWithStreamingResponse(self._client.extensions) @cached_property def browser_pools(self) -> browser_pools.BrowserPoolsResourceWithStreamingResponse: + """Create and manage browser pools for acquiring and releasing browsers.""" from .resources.browser_pools import BrowserPoolsResourceWithStreamingResponse return BrowserPoolsResourceWithStreamingResponse(self._client.browser_pools) @cached_property def credentials(self) -> credentials.CredentialsResourceWithStreamingResponse: + """Create and manage credentials for authentication.""" from .resources.credentials import CredentialsResourceWithStreamingResponse return CredentialsResourceWithStreamingResponse(self._client.credentials) @cached_property def credential_providers(self) -> credential_providers.CredentialProvidersResourceWithStreamingResponse: + """Configure external credential providers like 1Password.""" from .resources.credential_providers import CredentialProvidersResourceWithStreamingResponse return CredentialProvidersResourceWithStreamingResponse(self._client.credential_providers) @@ -821,30 +871,35 @@ def __init__(self, client: AsyncKernel) -> None: @cached_property def deployments(self) -> deployments.AsyncDeploymentsResourceWithStreamingResponse: + """Create and manage app deployments and stream deployment events.""" from .resources.deployments import AsyncDeploymentsResourceWithStreamingResponse return AsyncDeploymentsResourceWithStreamingResponse(self._client.deployments) @cached_property def apps(self) -> apps.AsyncAppsResourceWithStreamingResponse: + """List applications and versions.""" from .resources.apps import AsyncAppsResourceWithStreamingResponse return AsyncAppsResourceWithStreamingResponse(self._client.apps) @cached_property def invocations(self) -> invocations.AsyncInvocationsResourceWithStreamingResponse: + """Invoke actions and stream or query invocation status and events.""" from .resources.invocations import AsyncInvocationsResourceWithStreamingResponse return AsyncInvocationsResourceWithStreamingResponse(self._client.invocations) @cached_property def browsers(self) -> browsers.AsyncBrowsersResourceWithStreamingResponse: + """Create and manage browser sessions.""" from .resources.browsers import AsyncBrowsersResourceWithStreamingResponse return AsyncBrowsersResourceWithStreamingResponse(self._client.browsers) @cached_property def profiles(self) -> profiles.AsyncProfilesResourceWithStreamingResponse: + """Create, list, retrieve, and delete browser profiles.""" from .resources.profiles import AsyncProfilesResourceWithStreamingResponse return AsyncProfilesResourceWithStreamingResponse(self._client.profiles) @@ -857,30 +912,35 @@ def auth(self) -> auth.AsyncAuthResourceWithStreamingResponse: @cached_property def proxies(self) -> proxies.AsyncProxiesResourceWithStreamingResponse: + """Create and manage proxy configurations for routing browser traffic.""" from .resources.proxies import AsyncProxiesResourceWithStreamingResponse return AsyncProxiesResourceWithStreamingResponse(self._client.proxies) @cached_property def extensions(self) -> extensions.AsyncExtensionsResourceWithStreamingResponse: + """Create, list, retrieve, and delete browser extensions.""" from .resources.extensions import AsyncExtensionsResourceWithStreamingResponse return AsyncExtensionsResourceWithStreamingResponse(self._client.extensions) @cached_property def browser_pools(self) -> browser_pools.AsyncBrowserPoolsResourceWithStreamingResponse: + """Create and manage browser pools for acquiring and releasing browsers.""" from .resources.browser_pools import AsyncBrowserPoolsResourceWithStreamingResponse return AsyncBrowserPoolsResourceWithStreamingResponse(self._client.browser_pools) @cached_property def credentials(self) -> credentials.AsyncCredentialsResourceWithStreamingResponse: + """Create and manage credentials for authentication.""" from .resources.credentials import AsyncCredentialsResourceWithStreamingResponse return AsyncCredentialsResourceWithStreamingResponse(self._client.credentials) @cached_property def credential_providers(self) -> credential_providers.AsyncCredentialProvidersResourceWithStreamingResponse: + """Configure external credential providers like 1Password.""" from .resources.credential_providers import AsyncCredentialProvidersResourceWithStreamingResponse return AsyncCredentialProvidersResourceWithStreamingResponse(self._client.credential_providers) diff --git a/src/kernel/resources/apps.py b/src/kernel/resources/apps.py index 7383c29e..4290f3f6 100644 --- a/src/kernel/resources/apps.py +++ b/src/kernel/resources/apps.py @@ -23,6 +23,8 @@ class AppsResource(SyncAPIResource): + """List applications and versions.""" + @cached_property def with_raw_response(self) -> AppsResourceWithRawResponse: """ @@ -104,6 +106,8 @@ def list( class AsyncAppsResource(AsyncAPIResource): + """List applications and versions.""" + @cached_property def with_raw_response(self) -> AsyncAppsResourceWithRawResponse: """ diff --git a/src/kernel/resources/auth/auth.py b/src/kernel/resources/auth/auth.py index b5980e6b..f8744950 100644 --- a/src/kernel/resources/auth/auth.py +++ b/src/kernel/resources/auth/auth.py @@ -19,6 +19,7 @@ class AuthResource(SyncAPIResource): @cached_property def connections(self) -> ConnectionsResource: + """Create and manage auth connections for automated credential capture and login.""" return ConnectionsResource(self._client) @cached_property @@ -44,6 +45,7 @@ def with_streaming_response(self) -> AuthResourceWithStreamingResponse: class AsyncAuthResource(AsyncAPIResource): @cached_property def connections(self) -> AsyncConnectionsResource: + """Create and manage auth connections for automated credential capture and login.""" return AsyncConnectionsResource(self._client) @cached_property @@ -72,6 +74,7 @@ def __init__(self, auth: AuthResource) -> None: @cached_property def connections(self) -> ConnectionsResourceWithRawResponse: + """Create and manage auth connections for automated credential capture and login.""" return ConnectionsResourceWithRawResponse(self._auth.connections) @@ -81,6 +84,7 @@ def __init__(self, auth: AsyncAuthResource) -> None: @cached_property def connections(self) -> AsyncConnectionsResourceWithRawResponse: + """Create and manage auth connections for automated credential capture and login.""" return AsyncConnectionsResourceWithRawResponse(self._auth.connections) @@ -90,6 +94,7 @@ def __init__(self, auth: AuthResource) -> None: @cached_property def connections(self) -> ConnectionsResourceWithStreamingResponse: + """Create and manage auth connections for automated credential capture and login.""" return ConnectionsResourceWithStreamingResponse(self._auth.connections) @@ -99,4 +104,5 @@ def __init__(self, auth: AsyncAuthResource) -> None: @cached_property def connections(self) -> AsyncConnectionsResourceWithStreamingResponse: + """Create and manage auth connections for automated credential capture and login.""" return AsyncConnectionsResourceWithStreamingResponse(self._auth.connections) diff --git a/src/kernel/resources/auth/connections.py b/src/kernel/resources/auth/connections.py index 93b6cf6a..fed66797 100644 --- a/src/kernel/resources/auth/connections.py +++ b/src/kernel/resources/auth/connections.py @@ -34,6 +34,8 @@ class ConnectionsResource(SyncAPIResource): + """Create and manage auth connections for automated credential capture and login.""" + @cached_property def with_raw_response(self) -> ConnectionsResourceWithRawResponse: """ @@ -413,6 +415,8 @@ def submit( class AsyncConnectionsResource(AsyncAPIResource): + """Create and manage auth connections for automated credential capture and login.""" + @cached_property def with_raw_response(self) -> AsyncConnectionsResourceWithRawResponse: """ diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py index 9177d678..a5b6e59a 100644 --- a/src/kernel/resources/browser_pools.py +++ b/src/kernel/resources/browser_pools.py @@ -35,6 +35,8 @@ class BrowserPoolsResource(SyncAPIResource): + """Create and manage browser pools for acquiring and releasing browsers.""" + @cached_property def with_raw_response(self) -> BrowserPoolsResourceWithRawResponse: """ @@ -473,6 +475,8 @@ def release( class AsyncBrowserPoolsResource(AsyncAPIResource): + """Create and manage browser pools for acquiring and releasing browsers.""" + @cached_property def with_raw_response(self) -> AsyncBrowserPoolsResourceWithRawResponse: """ diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 59cecda1..32855ee8 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -89,20 +89,26 @@ class BrowsersResource(SyncAPIResource): + """Create and manage browser sessions.""" + @cached_property def replays(self) -> ReplaysResource: + """Record and manage browser session video replays.""" return ReplaysResource(self._client) @cached_property def fs(self) -> FsResource: + """Read, write, and manage files on the browser instance.""" return FsResource(self._client) @cached_property def process(self) -> ProcessResource: + """Execute and manage processes on the browser instance.""" return ProcessResource(self._client) @cached_property def logs(self) -> LogsResource: + """Stream logs from the browser instance.""" return LogsResource(self._client) @cached_property @@ -111,6 +117,7 @@ def computer(self) -> ComputerResource: @cached_property def playwright(self) -> PlaywrightResource: + """Execute Playwright code against the browser instance.""" return PlaywrightResource(self._client) @cached_property @@ -509,20 +516,26 @@ def load_extensions( class AsyncBrowsersResource(AsyncAPIResource): + """Create and manage browser sessions.""" + @cached_property def replays(self) -> AsyncReplaysResource: + """Record and manage browser session video replays.""" return AsyncReplaysResource(self._client) @cached_property def fs(self) -> AsyncFsResource: + """Read, write, and manage files on the browser instance.""" return AsyncFsResource(self._client) @cached_property def process(self) -> AsyncProcessResource: + """Execute and manage processes on the browser instance.""" return AsyncProcessResource(self._client) @cached_property def logs(self) -> AsyncLogsResource: + """Stream logs from the browser instance.""" return AsyncLogsResource(self._client) @cached_property @@ -531,6 +544,7 @@ def computer(self) -> AsyncComputerResource: @cached_property def playwright(self) -> AsyncPlaywrightResource: + """Execute Playwright code against the browser instance.""" return AsyncPlaywrightResource(self._client) @cached_property @@ -960,18 +974,22 @@ def __init__(self, browsers: BrowsersResource) -> None: @cached_property def replays(self) -> ReplaysResourceWithRawResponse: + """Record and manage browser session video replays.""" return ReplaysResourceWithRawResponse(self._browsers.replays) @cached_property def fs(self) -> FsResourceWithRawResponse: + """Read, write, and manage files on the browser instance.""" return FsResourceWithRawResponse(self._browsers.fs) @cached_property def process(self) -> ProcessResourceWithRawResponse: + """Execute and manage processes on the browser instance.""" return ProcessResourceWithRawResponse(self._browsers.process) @cached_property def logs(self) -> LogsResourceWithRawResponse: + """Stream logs from the browser instance.""" return LogsResourceWithRawResponse(self._browsers.logs) @cached_property @@ -980,6 +998,7 @@ def computer(self) -> ComputerResourceWithRawResponse: @cached_property def playwright(self) -> PlaywrightResourceWithRawResponse: + """Execute Playwright code against the browser instance.""" return PlaywrightResourceWithRawResponse(self._browsers.playwright) @@ -1013,18 +1032,22 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None: @cached_property def replays(self) -> AsyncReplaysResourceWithRawResponse: + """Record and manage browser session video replays.""" return AsyncReplaysResourceWithRawResponse(self._browsers.replays) @cached_property def fs(self) -> AsyncFsResourceWithRawResponse: + """Read, write, and manage files on the browser instance.""" return AsyncFsResourceWithRawResponse(self._browsers.fs) @cached_property def process(self) -> AsyncProcessResourceWithRawResponse: + """Execute and manage processes on the browser instance.""" return AsyncProcessResourceWithRawResponse(self._browsers.process) @cached_property def logs(self) -> AsyncLogsResourceWithRawResponse: + """Stream logs from the browser instance.""" return AsyncLogsResourceWithRawResponse(self._browsers.logs) @cached_property @@ -1033,6 +1056,7 @@ def computer(self) -> AsyncComputerResourceWithRawResponse: @cached_property def playwright(self) -> AsyncPlaywrightResourceWithRawResponse: + """Execute Playwright code against the browser instance.""" return AsyncPlaywrightResourceWithRawResponse(self._browsers.playwright) @@ -1066,18 +1090,22 @@ def __init__(self, browsers: BrowsersResource) -> None: @cached_property def replays(self) -> ReplaysResourceWithStreamingResponse: + """Record and manage browser session video replays.""" return ReplaysResourceWithStreamingResponse(self._browsers.replays) @cached_property def fs(self) -> FsResourceWithStreamingResponse: + """Read, write, and manage files on the browser instance.""" return FsResourceWithStreamingResponse(self._browsers.fs) @cached_property def process(self) -> ProcessResourceWithStreamingResponse: + """Execute and manage processes on the browser instance.""" return ProcessResourceWithStreamingResponse(self._browsers.process) @cached_property def logs(self) -> LogsResourceWithStreamingResponse: + """Stream logs from the browser instance.""" return LogsResourceWithStreamingResponse(self._browsers.logs) @cached_property @@ -1086,6 +1114,7 @@ def computer(self) -> ComputerResourceWithStreamingResponse: @cached_property def playwright(self) -> PlaywrightResourceWithStreamingResponse: + """Execute Playwright code against the browser instance.""" return PlaywrightResourceWithStreamingResponse(self._browsers.playwright) @@ -1119,18 +1148,22 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None: @cached_property def replays(self) -> AsyncReplaysResourceWithStreamingResponse: + """Record and manage browser session video replays.""" return AsyncReplaysResourceWithStreamingResponse(self._browsers.replays) @cached_property def fs(self) -> AsyncFsResourceWithStreamingResponse: + """Read, write, and manage files on the browser instance.""" return AsyncFsResourceWithStreamingResponse(self._browsers.fs) @cached_property def process(self) -> AsyncProcessResourceWithStreamingResponse: + """Execute and manage processes on the browser instance.""" return AsyncProcessResourceWithStreamingResponse(self._browsers.process) @cached_property def logs(self) -> AsyncLogsResourceWithStreamingResponse: + """Stream logs from the browser instance.""" return AsyncLogsResourceWithStreamingResponse(self._browsers.logs) @cached_property @@ -1139,4 +1172,5 @@ def computer(self) -> AsyncComputerResourceWithStreamingResponse: @cached_property def playwright(self) -> AsyncPlaywrightResourceWithStreamingResponse: + """Execute Playwright code against the browser instance.""" return AsyncPlaywrightResourceWithStreamingResponse(self._browsers.playwright) diff --git a/src/kernel/resources/browsers/fs/fs.py b/src/kernel/resources/browsers/fs/fs.py index 3501a2a6..1bd16afb 100644 --- a/src/kernel/resources/browsers/fs/fs.py +++ b/src/kernel/resources/browsers/fs/fs.py @@ -69,8 +69,11 @@ class FsResource(SyncAPIResource): + """Read, write, and manage files on the browser instance.""" + @cached_property def watch(self) -> WatchResource: + """Read, write, and manage files on the browser instance.""" return WatchResource(self._client) @cached_property @@ -628,8 +631,11 @@ def write_file( class AsyncFsResource(AsyncAPIResource): + """Read, write, and manage files on the browser instance.""" + @cached_property def watch(self) -> AsyncWatchResource: + """Read, write, and manage files on the browser instance.""" return AsyncWatchResource(self._client) @cached_property @@ -1231,6 +1237,7 @@ def __init__(self, fs: FsResource) -> None: @cached_property def watch(self) -> WatchResourceWithRawResponse: + """Read, write, and manage files on the browser instance.""" return WatchResourceWithRawResponse(self._fs.watch) @@ -1279,6 +1286,7 @@ def __init__(self, fs: AsyncFsResource) -> None: @cached_property def watch(self) -> AsyncWatchResourceWithRawResponse: + """Read, write, and manage files on the browser instance.""" return AsyncWatchResourceWithRawResponse(self._fs.watch) @@ -1327,6 +1335,7 @@ def __init__(self, fs: FsResource) -> None: @cached_property def watch(self) -> WatchResourceWithStreamingResponse: + """Read, write, and manage files on the browser instance.""" return WatchResourceWithStreamingResponse(self._fs.watch) @@ -1375,4 +1384,5 @@ def __init__(self, fs: AsyncFsResource) -> None: @cached_property def watch(self) -> AsyncWatchResourceWithStreamingResponse: + """Read, write, and manage files on the browser instance.""" return AsyncWatchResourceWithStreamingResponse(self._fs.watch) diff --git a/src/kernel/resources/browsers/fs/watch.py b/src/kernel/resources/browsers/fs/watch.py index 2a5c1e30..ca438673 100644 --- a/src/kernel/resources/browsers/fs/watch.py +++ b/src/kernel/resources/browsers/fs/watch.py @@ -24,6 +24,8 @@ class WatchResource(SyncAPIResource): + """Read, write, and manage files on the browser instance.""" + @cached_property def with_raw_response(self) -> WatchResourceWithRawResponse: """ @@ -167,6 +169,8 @@ def stop( class AsyncWatchResource(AsyncAPIResource): + """Read, write, and manage files on the browser instance.""" + @cached_property def with_raw_response(self) -> AsyncWatchResourceWithRawResponse: """ diff --git a/src/kernel/resources/browsers/logs.py b/src/kernel/resources/browsers/logs.py index ab97a70d..01328551 100644 --- a/src/kernel/resources/browsers/logs.py +++ b/src/kernel/resources/browsers/logs.py @@ -25,6 +25,8 @@ class LogsResource(SyncAPIResource): + """Stream logs from the browser instance.""" + @cached_property def with_raw_response(self) -> LogsResourceWithRawResponse: """ @@ -102,6 +104,8 @@ def stream( class AsyncLogsResource(AsyncAPIResource): + """Stream logs from the browser instance.""" + @cached_property def with_raw_response(self) -> AsyncLogsResourceWithRawResponse: """ diff --git a/src/kernel/resources/browsers/playwright.py b/src/kernel/resources/browsers/playwright.py index 5c47e3bf..6979a9de 100644 --- a/src/kernel/resources/browsers/playwright.py +++ b/src/kernel/resources/browsers/playwright.py @@ -22,6 +22,8 @@ class PlaywrightResource(SyncAPIResource): + """Execute Playwright code against the browser instance.""" + @cached_property def with_raw_response(self) -> PlaywrightResourceWithRawResponse: """ @@ -96,6 +98,8 @@ def execute( class AsyncPlaywrightResource(AsyncAPIResource): + """Execute Playwright code against the browser instance.""" + @cached_property def with_raw_response(self) -> AsyncPlaywrightResourceWithRawResponse: """ diff --git a/src/kernel/resources/browsers/process.py b/src/kernel/resources/browsers/process.py index 9932f40c..86752a5e 100644 --- a/src/kernel/resources/browsers/process.py +++ b/src/kernel/resources/browsers/process.py @@ -38,6 +38,8 @@ class ProcessResource(SyncAPIResource): + """Execute and manage processes on the browser instance.""" + @cached_property def with_raw_response(self) -> ProcessResourceWithRawResponse: """ @@ -407,6 +409,8 @@ def stdout_stream( class AsyncProcessResource(AsyncAPIResource): + """Execute and manage processes on the browser instance.""" + @cached_property def with_raw_response(self) -> AsyncProcessResourceWithRawResponse: """ diff --git a/src/kernel/resources/browsers/replays.py b/src/kernel/resources/browsers/replays.py index 8a1d1996..743a6668 100644 --- a/src/kernel/resources/browsers/replays.py +++ b/src/kernel/resources/browsers/replays.py @@ -31,6 +31,8 @@ class ReplaysResource(SyncAPIResource): + """Record and manage browser session video replays.""" + @cached_property def with_raw_response(self) -> ReplaysResourceWithRawResponse: """ @@ -205,6 +207,8 @@ def stop( class AsyncReplaysResource(AsyncAPIResource): + """Record and manage browser session video replays.""" + @cached_property def with_raw_response(self) -> AsyncReplaysResourceWithRawResponse: """ diff --git a/src/kernel/resources/credential_providers.py b/src/kernel/resources/credential_providers.py index 8df7d55c..c7ad4b00 100644 --- a/src/kernel/resources/credential_providers.py +++ b/src/kernel/resources/credential_providers.py @@ -27,6 +27,8 @@ class CredentialProvidersResource(SyncAPIResource): + """Configure external credential providers like 1Password.""" + @cached_property def with_raw_response(self) -> CredentialProvidersResourceWithRawResponse: """ @@ -311,6 +313,8 @@ def test( class AsyncCredentialProvidersResource(AsyncAPIResource): + """Configure external credential providers like 1Password.""" + @cached_property def with_raw_response(self) -> AsyncCredentialProvidersResourceWithRawResponse: """ diff --git a/src/kernel/resources/credentials.py b/src/kernel/resources/credentials.py index 30e72e84..000c7675 100644 --- a/src/kernel/resources/credentials.py +++ b/src/kernel/resources/credentials.py @@ -26,6 +26,8 @@ class CredentialsResource(SyncAPIResource): + """Create and manage credentials for authentication.""" + @cached_property def with_raw_response(self) -> CredentialsResourceWithRawResponse: """ @@ -321,6 +323,8 @@ def totp_code( class AsyncCredentialsResource(AsyncAPIResource): + """Create and manage credentials for authentication.""" + @cached_property def with_raw_response(self) -> AsyncCredentialsResourceWithRawResponse: """ diff --git a/src/kernel/resources/deployments.py b/src/kernel/resources/deployments.py index 753eb8b1..b6a72d2d 100644 --- a/src/kernel/resources/deployments.py +++ b/src/kernel/resources/deployments.py @@ -30,6 +30,8 @@ class DeploymentsResource(SyncAPIResource): + """Create and manage app deployments and stream deployment events.""" + @cached_property def with_raw_response(self) -> DeploymentsResourceWithRawResponse: """ @@ -293,6 +295,8 @@ def follow( class AsyncDeploymentsResource(AsyncAPIResource): + """Create and manage app deployments and stream deployment events.""" + @cached_property def with_raw_response(self) -> AsyncDeploymentsResourceWithRawResponse: """ diff --git a/src/kernel/resources/extensions.py b/src/kernel/resources/extensions.py index 69497b1f..ffdef29e 100644 --- a/src/kernel/resources/extensions.py +++ b/src/kernel/resources/extensions.py @@ -34,6 +34,8 @@ class ExtensionsResource(SyncAPIResource): + """Create, list, retrieve, and delete browser extensions.""" + @cached_property def with_raw_response(self) -> ExtensionsResourceWithRawResponse: """ @@ -241,6 +243,8 @@ def upload( class AsyncExtensionsResource(AsyncAPIResource): + """Create, list, retrieve, and delete browser extensions.""" + @cached_property def with_raw_response(self) -> AsyncExtensionsResourceWithRawResponse: """ diff --git a/src/kernel/resources/invocations.py b/src/kernel/resources/invocations.py index 3194026d..25be409f 100644 --- a/src/kernel/resources/invocations.py +++ b/src/kernel/resources/invocations.py @@ -32,6 +32,8 @@ class InvocationsResource(SyncAPIResource): + """Invoke actions and stream or query invocation status and events.""" + @cached_property def with_raw_response(self) -> InvocationsResourceWithRawResponse: """ @@ -383,6 +385,8 @@ def list_browsers( class AsyncInvocationsResource(AsyncAPIResource): + """Invoke actions and stream or query invocation status and events.""" + @cached_property def with_raw_response(self) -> AsyncInvocationsResourceWithRawResponse: """ diff --git a/src/kernel/resources/profiles.py b/src/kernel/resources/profiles.py index e9b124b5..f75569f2 100644 --- a/src/kernel/resources/profiles.py +++ b/src/kernel/resources/profiles.py @@ -31,6 +31,8 @@ class ProfilesResource(SyncAPIResource): + """Create, list, retrieve, and delete browser profiles.""" + @cached_property def with_raw_response(self) -> ProfilesResourceWithRawResponse: """ @@ -241,6 +243,8 @@ def download( class AsyncProfilesResource(AsyncAPIResource): + """Create, list, retrieve, and delete browser profiles.""" + @cached_property def with_raw_response(self) -> AsyncProfilesResourceWithRawResponse: """ diff --git a/src/kernel/resources/proxies.py b/src/kernel/resources/proxies.py index d42f7b04..0c2508c0 100644 --- a/src/kernel/resources/proxies.py +++ b/src/kernel/resources/proxies.py @@ -27,6 +27,8 @@ class ProxiesResource(SyncAPIResource): + """Create and manage proxy configurations for routing browser traffic.""" + @cached_property def with_raw_response(self) -> ProxiesResourceWithRawResponse: """ @@ -224,6 +226,8 @@ def check( class AsyncProxiesResource(AsyncAPIResource): + """Create and manage proxy configurations for routing browser traffic.""" + @cached_property def with_raw_response(self) -> AsyncProxiesResourceWithRawResponse: """ From 038133ed9adb5028b56a40cc6437388c8ceb7bf7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:32:31 +0000 Subject: [PATCH 327/448] feat: [kernel-1028] add api clipboard support --- .stats.yml | 8 +- api.md | 3 + src/kernel/resources/browsers/computer.py | 170 +++++++++++++++++ src/kernel/types/browsers/__init__.py | 2 + .../computer_read_clipboard_response.py | 10 + .../computer_write_clipboard_params.py | 12 ++ tests/api_resources/browsers/test_computer.py | 177 ++++++++++++++++++ 7 files changed, 378 insertions(+), 4 deletions(-) create mode 100644 src/kernel/types/browsers/computer_read_clipboard_response.py create mode 100644 src/kernel/types/browsers/computer_write_clipboard_params.py diff --git a/.stats.yml b/.stats.yml index d4eead98..a08c103b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 101 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-a935c8aae21f8ddb83ea5e289034df12cbde88d432fa2b287629814bb3f58bb6.yml -openapi_spec_hash: df3189b9728372f01662a19c060bcbc5 -config_hash: 81f143f4bee47ae7b0b8357551babadf +configured_endpoints: 103 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-f05b888046776a18dbffc1264a27c0256839d132066ef5f6e09ccf1bc505a8f7.yml +openapi_spec_hash: 646fce3982d3efbdb38004b0e4ac4d17 +config_hash: cff4d43372b6fa66b64e2d4150f6aa76 diff --git a/api.md b/api.md index 511ab51f..cce73021 100644 --- a/api.md +++ b/api.md @@ -192,6 +192,7 @@ Types: ```python from kernel.types.browsers import ( ComputerGetMousePositionResponse, + ComputerReadClipboardResponse, ComputerSetCursorVisibilityResponse, ) ``` @@ -205,9 +206,11 @@ Methods: - client.browsers.computer.get_mouse_position(id) -> ComputerGetMousePositionResponse - client.browsers.computer.move_mouse(id, \*\*params) -> None - client.browsers.computer.press_key(id, \*\*params) -> None +- client.browsers.computer.read_clipboard(id) -> ComputerReadClipboardResponse - client.browsers.computer.scroll(id, \*\*params) -> None - client.browsers.computer.set_cursor_visibility(id, \*\*params) -> ComputerSetCursorVisibilityResponse - client.browsers.computer.type_text(id, \*\*params) -> None +- client.browsers.computer.write_clipboard(id, \*\*params) -> None ## Playwright diff --git a/src/kernel/resources/browsers/computer.py b/src/kernel/resources/browsers/computer.py index 933767f0..cc61834b 100644 --- a/src/kernel/resources/browsers/computer.py +++ b/src/kernel/resources/browsers/computer.py @@ -34,9 +34,11 @@ computer_drag_mouse_params, computer_move_mouse_params, computer_click_mouse_params, + computer_write_clipboard_params, computer_capture_screenshot_params, computer_set_cursor_visibility_params, ) +from ...types.browsers.computer_read_clipboard_response import ComputerReadClipboardResponse from ...types.browsers.computer_get_mouse_position_response import ComputerGetMousePositionResponse from ...types.browsers.computer_set_cursor_visibility_response import ComputerSetCursorVisibilityResponse @@ -408,6 +410,39 @@ def press_key( cast_to=NoneType, ) + def read_clipboard( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ComputerReadClipboardResponse: + """ + Read text from the clipboard on the browser instance + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/browsers/{id}/computer/clipboard/read", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ComputerReadClipboardResponse, + ) + def scroll( self, id: str, @@ -553,6 +588,44 @@ def type_text( cast_to=NoneType, ) + def write_clipboard( + self, + id: str, + *, + text: 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: + """ + Write text to the clipboard on the browser instance + + Args: + text: Text to write to the system clipboard + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._post( + f"/browsers/{id}/computer/clipboard/write", + body=maybe_transform({"text": text}, computer_write_clipboard_params.ComputerWriteClipboardParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + class AsyncComputerResource(AsyncAPIResource): @cached_property @@ -919,6 +992,39 @@ async def press_key( cast_to=NoneType, ) + async def read_clipboard( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ComputerReadClipboardResponse: + """ + Read text from the clipboard on the browser instance + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/browsers/{id}/computer/clipboard/read", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ComputerReadClipboardResponse, + ) + async def scroll( self, id: str, @@ -1064,6 +1170,46 @@ async def type_text( cast_to=NoneType, ) + async def write_clipboard( + self, + id: str, + *, + text: 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: + """ + Write text to the clipboard on the browser instance + + Args: + text: Text to write to the system clipboard + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._post( + f"/browsers/{id}/computer/clipboard/write", + body=await async_maybe_transform( + {"text": text}, computer_write_clipboard_params.ComputerWriteClipboardParams + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + class ComputerResourceWithRawResponse: def __init__(self, computer: ComputerResource) -> None: @@ -1091,6 +1237,9 @@ def __init__(self, computer: ComputerResource) -> None: self.press_key = to_raw_response_wrapper( computer.press_key, ) + self.read_clipboard = to_raw_response_wrapper( + computer.read_clipboard, + ) self.scroll = to_raw_response_wrapper( computer.scroll, ) @@ -1100,6 +1249,9 @@ def __init__(self, computer: ComputerResource) -> None: self.type_text = to_raw_response_wrapper( computer.type_text, ) + self.write_clipboard = to_raw_response_wrapper( + computer.write_clipboard, + ) class AsyncComputerResourceWithRawResponse: @@ -1128,6 +1280,9 @@ def __init__(self, computer: AsyncComputerResource) -> None: self.press_key = async_to_raw_response_wrapper( computer.press_key, ) + self.read_clipboard = async_to_raw_response_wrapper( + computer.read_clipboard, + ) self.scroll = async_to_raw_response_wrapper( computer.scroll, ) @@ -1137,6 +1292,9 @@ def __init__(self, computer: AsyncComputerResource) -> None: self.type_text = async_to_raw_response_wrapper( computer.type_text, ) + self.write_clipboard = async_to_raw_response_wrapper( + computer.write_clipboard, + ) class ComputerResourceWithStreamingResponse: @@ -1165,6 +1323,9 @@ def __init__(self, computer: ComputerResource) -> None: self.press_key = to_streamed_response_wrapper( computer.press_key, ) + self.read_clipboard = to_streamed_response_wrapper( + computer.read_clipboard, + ) self.scroll = to_streamed_response_wrapper( computer.scroll, ) @@ -1174,6 +1335,9 @@ def __init__(self, computer: ComputerResource) -> None: self.type_text = to_streamed_response_wrapper( computer.type_text, ) + self.write_clipboard = to_streamed_response_wrapper( + computer.write_clipboard, + ) class AsyncComputerResourceWithStreamingResponse: @@ -1202,6 +1366,9 @@ def __init__(self, computer: AsyncComputerResource) -> None: self.press_key = async_to_streamed_response_wrapper( computer.press_key, ) + self.read_clipboard = async_to_streamed_response_wrapper( + computer.read_clipboard, + ) self.scroll = async_to_streamed_response_wrapper( computer.scroll, ) @@ -1211,3 +1378,6 @@ def __init__(self, computer: AsyncComputerResource) -> None: self.type_text = async_to_streamed_response_wrapper( computer.type_text, ) + self.write_clipboard = async_to_streamed_response_wrapper( + computer.write_clipboard, + ) diff --git a/src/kernel/types/browsers/__init__.py b/src/kernel/types/browsers/__init__.py index 3daee051..1e47205d 100644 --- a/src/kernel/types/browsers/__init__.py +++ b/src/kernel/types/browsers/__init__.py @@ -41,6 +41,8 @@ from .playwright_execute_response import PlaywrightExecuteResponse as PlaywrightExecuteResponse from .f_set_file_permissions_params import FSetFilePermissionsParams as FSetFilePermissionsParams from .process_stdout_stream_response import ProcessStdoutStreamResponse as ProcessStdoutStreamResponse +from .computer_write_clipboard_params import ComputerWriteClipboardParams as ComputerWriteClipboardParams +from .computer_read_clipboard_response import ComputerReadClipboardResponse as ComputerReadClipboardResponse from .computer_capture_screenshot_params import ComputerCaptureScreenshotParams as ComputerCaptureScreenshotParams from .computer_get_mouse_position_response import ComputerGetMousePositionResponse as ComputerGetMousePositionResponse from .computer_set_cursor_visibility_params import ( diff --git a/src/kernel/types/browsers/computer_read_clipboard_response.py b/src/kernel/types/browsers/computer_read_clipboard_response.py new file mode 100644 index 00000000..e5210090 --- /dev/null +++ b/src/kernel/types/browsers/computer_read_clipboard_response.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from ..._models import BaseModel + +__all__ = ["ComputerReadClipboardResponse"] + + +class ComputerReadClipboardResponse(BaseModel): + text: str + """Current clipboard text content""" diff --git a/src/kernel/types/browsers/computer_write_clipboard_params.py b/src/kernel/types/browsers/computer_write_clipboard_params.py new file mode 100644 index 00000000..81f7b095 --- /dev/null +++ b/src/kernel/types/browsers/computer_write_clipboard_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["ComputerWriteClipboardParams"] + + +class ComputerWriteClipboardParams(TypedDict, total=False): + text: Required[str] + """Text to write to the system clipboard""" diff --git a/tests/api_resources/browsers/test_computer.py b/tests/api_resources/browsers/test_computer.py index 091fc91d..32a4ca9c 100644 --- a/tests/api_resources/browsers/test_computer.py +++ b/tests/api_resources/browsers/test_computer.py @@ -18,6 +18,7 @@ AsyncStreamedBinaryAPIResponse, ) from kernel.types.browsers import ( + ComputerReadClipboardResponse, ComputerGetMousePositionResponse, ComputerSetCursorVisibilityResponse, ) @@ -426,6 +427,48 @@ def test_path_params_press_key(self, client: Kernel) -> None: keys=["string"], ) + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_read_clipboard(self, client: Kernel) -> None: + computer = client.browsers.computer.read_clipboard( + "id", + ) + assert_matches_type(ComputerReadClipboardResponse, computer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_read_clipboard(self, client: Kernel) -> None: + response = client.browsers.computer.with_raw_response.read_clipboard( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + computer = response.parse() + assert_matches_type(ComputerReadClipboardResponse, computer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_read_clipboard(self, client: Kernel) -> None: + with client.browsers.computer.with_streaming_response.read_clipboard( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + computer = response.parse() + assert_matches_type(ComputerReadClipboardResponse, computer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_read_clipboard(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.computer.with_raw_response.read_clipboard( + "", + ) + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_scroll(self, client: Kernel) -> None: @@ -591,6 +634,52 @@ def test_path_params_type_text(self, client: Kernel) -> None: text="text", ) + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_write_clipboard(self, client: Kernel) -> None: + computer = client.browsers.computer.write_clipboard( + id="id", + text="text", + ) + assert computer is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_write_clipboard(self, client: Kernel) -> None: + response = client.browsers.computer.with_raw_response.write_clipboard( + id="id", + text="text", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + computer = response.parse() + assert computer is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_write_clipboard(self, client: Kernel) -> None: + with client.browsers.computer.with_streaming_response.write_clipboard( + id="id", + text="text", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + computer = response.parse() + assert computer is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_write_clipboard(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.computer.with_raw_response.write_clipboard( + id="", + text="text", + ) + class TestAsyncComputer: parametrize = pytest.mark.parametrize( @@ -999,6 +1088,48 @@ async def test_path_params_press_key(self, async_client: AsyncKernel) -> None: keys=["string"], ) + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_read_clipboard(self, async_client: AsyncKernel) -> None: + computer = await async_client.browsers.computer.read_clipboard( + "id", + ) + assert_matches_type(ComputerReadClipboardResponse, computer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_read_clipboard(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.computer.with_raw_response.read_clipboard( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + computer = await response.parse() + assert_matches_type(ComputerReadClipboardResponse, computer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_read_clipboard(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.computer.with_streaming_response.read_clipboard( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + computer = await response.parse() + assert_matches_type(ComputerReadClipboardResponse, computer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_read_clipboard(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.computer.with_raw_response.read_clipboard( + "", + ) + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_scroll(self, async_client: AsyncKernel) -> None: @@ -1163,3 +1294,49 @@ async def test_path_params_type_text(self, async_client: AsyncKernel) -> None: id="", text="text", ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_write_clipboard(self, async_client: AsyncKernel) -> None: + computer = await async_client.browsers.computer.write_clipboard( + id="id", + text="text", + ) + assert computer is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_write_clipboard(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.computer.with_raw_response.write_clipboard( + id="id", + text="text", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + computer = await response.parse() + assert computer is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_write_clipboard(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.computer.with_streaming_response.write_clipboard( + id="id", + text="text", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + computer = await response.parse() + assert computer is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_write_clipboard(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.computer.with_raw_response.write_clipboard( + id="", + text="text", + ) From a6db2a541de6e8b30560c9d848eef9cdd887a2e3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 22:37:29 +0000 Subject: [PATCH 328/448] feat: expose smooth mouse movement via public API --- .stats.yml | 4 ++-- src/kernel/resources/browsers/computer.py | 18 ++++++++++++++++++ .../types/browsers/computer_batch_params.py | 9 +++++++++ .../browsers/computer_move_mouse_params.py | 9 +++++++++ tests/api_resources/browsers/test_computer.py | 4 ++++ 5 files changed, 42 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index a08c103b..d2f972b9 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 103 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-f05b888046776a18dbffc1264a27c0256839d132066ef5f6e09ccf1bc505a8f7.yml -openapi_spec_hash: 646fce3982d3efbdb38004b0e4ac4d17 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2ba004ce5444b5f8abe3bcf66fd7c6da394bc964e8b2bf197576841135a48046.yml +openapi_spec_hash: f156ea2ae35e4d148704c6e4ce051239 config_hash: cff4d43372b6fa66b64e2d4150f6aa76 diff --git a/src/kernel/resources/browsers/computer.py b/src/kernel/resources/browsers/computer.py index cc61834b..1357c1e8 100644 --- a/src/kernel/resources/browsers/computer.py +++ b/src/kernel/resources/browsers/computer.py @@ -310,7 +310,9 @@ def move_mouse( *, x: int, y: int, + duration_ms: int | Omit = omit, hold_keys: SequenceNotStr[str] | Omit = omit, + smooth: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -326,8 +328,13 @@ def move_mouse( y: Y coordinate to move the cursor to + duration_ms: Target total duration in milliseconds for the mouse movement when smooth=true. + Omit for automatic timing based on distance. + hold_keys: Modifier keys to hold during the move + smooth: Use human-like Bezier curve path instead of instant mouse movement. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -345,7 +352,9 @@ def move_mouse( { "x": x, "y": y, + "duration_ms": duration_ms, "hold_keys": hold_keys, + "smooth": smooth, }, computer_move_mouse_params.ComputerMoveMouseParams, ), @@ -892,7 +901,9 @@ async def move_mouse( *, x: int, y: int, + duration_ms: int | Omit = omit, hold_keys: SequenceNotStr[str] | Omit = omit, + smooth: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -908,8 +919,13 @@ async def move_mouse( y: Y coordinate to move the cursor to + duration_ms: Target total duration in milliseconds for the mouse movement when smooth=true. + Omit for automatic timing based on distance. + hold_keys: Modifier keys to hold during the move + smooth: Use human-like Bezier curve path instead of instant mouse movement. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -927,7 +943,9 @@ async def move_mouse( { "x": x, "y": y, + "duration_ms": duration_ms, "hold_keys": hold_keys, + "smooth": smooth, }, computer_move_mouse_params.ComputerMoveMouseParams, ), diff --git a/src/kernel/types/browsers/computer_batch_params.py b/src/kernel/types/browsers/computer_batch_params.py index 601cd2b9..9aca7244 100644 --- a/src/kernel/types/browsers/computer_batch_params.py +++ b/src/kernel/types/browsers/computer_batch_params.py @@ -79,9 +79,18 @@ class ActionMoveMouse(TypedDict, total=False): y: Required[int] """Y coordinate to move the cursor to""" + duration_ms: int + """Target total duration in milliseconds for the mouse movement when smooth=true. + + Omit for automatic timing based on distance. + """ + hold_keys: SequenceNotStr[str] """Modifier keys to hold during the move""" + smooth: bool + """Use human-like Bezier curve path instead of instant mouse movement.""" + class ActionPressKey(TypedDict, total=False): keys: Required[SequenceNotStr[str]] diff --git a/src/kernel/types/browsers/computer_move_mouse_params.py b/src/kernel/types/browsers/computer_move_mouse_params.py index 1769e074..3a4f99e5 100644 --- a/src/kernel/types/browsers/computer_move_mouse_params.py +++ b/src/kernel/types/browsers/computer_move_mouse_params.py @@ -16,5 +16,14 @@ class ComputerMoveMouseParams(TypedDict, total=False): y: Required[int] """Y coordinate to move the cursor to""" + duration_ms: int + """Target total duration in milliseconds for the mouse movement when smooth=true. + + Omit for automatic timing based on distance. + """ + hold_keys: SequenceNotStr[str] """Modifier keys to hold during the move""" + + smooth: bool + """Use human-like Bezier curve path instead of instant mouse movement.""" diff --git a/tests/api_resources/browsers/test_computer.py b/tests/api_resources/browsers/test_computer.py index 32a4ca9c..09960bfc 100644 --- a/tests/api_resources/browsers/test_computer.py +++ b/tests/api_resources/browsers/test_computer.py @@ -326,7 +326,9 @@ def test_method_move_mouse_with_all_params(self, client: Kernel) -> None: id="id", x=0, y=0, + duration_ms=50, hold_keys=["string"], + smooth=True, ) assert computer is None @@ -987,7 +989,9 @@ async def test_method_move_mouse_with_all_params(self, async_client: AsyncKernel id="id", x=0, y=0, + duration_ms=50, hold_keys=["string"], + smooth=True, ) assert computer is None From 5243db51d1f58496e7ecfadf7e16d88311c5fc4a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:43:01 +0000 Subject: [PATCH 329/448] feat: add force flag to viewport resize to bypass live view/recording check --- .stats.yml | 4 ++-- src/kernel/resources/browsers/browsers.py | 4 ++-- src/kernel/types/browser_update_params.py | 16 ++++++++++++++-- tests/api_resources/test_browsers.py | 2 ++ 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/.stats.yml b/.stats.yml index d2f972b9..e1ce185b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 103 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2ba004ce5444b5f8abe3bcf66fd7c6da394bc964e8b2bf197576841135a48046.yml -openapi_spec_hash: f156ea2ae35e4d148704c6e4ce051239 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-ef24d4bf172555bcbe8e3b432c644a25a1c6afd99c958a2eda8c3b1ea9568113.yml +openapi_spec_hash: b603c5a983e837928fa7d1100ed64fc9 config_hash: cff4d43372b6fa66b64e2d4150f6aa76 diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 32855ee8..235da236 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -284,7 +284,7 @@ def update( *, profile: BrowserProfile | Omit = omit, proxy_id: Optional[str] | Omit = omit, - viewport: BrowserViewport | Omit = omit, + viewport: browser_update_params.Viewport | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -711,7 +711,7 @@ async def update( *, profile: BrowserProfile | Omit = omit, proxy_id: Optional[str] | Omit = omit, - viewport: BrowserViewport | Omit = omit, + viewport: browser_update_params.Viewport | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, diff --git a/src/kernel/types/browser_update_params.py b/src/kernel/types/browser_update_params.py index 917cd7da..5c211948 100644 --- a/src/kernel/types/browser_update_params.py +++ b/src/kernel/types/browser_update_params.py @@ -8,7 +8,7 @@ from .shared_params.browser_profile import BrowserProfile from .shared_params.browser_viewport import BrowserViewport -__all__ = ["BrowserUpdateParams"] +__all__ = ["BrowserUpdateParams", "Viewport"] class BrowserUpdateParams(TypedDict, total=False): @@ -24,5 +24,17 @@ class BrowserUpdateParams(TypedDict, total=False): Omit to leave unchanged, set to empty string to remove proxy. """ - viewport: BrowserViewport + viewport: Viewport """Viewport configuration to apply to the browser session.""" + + +class Viewport(BrowserViewport, total=False): + """Viewport configuration to apply to the browser session.""" + + force: bool + """ + If true, allow the viewport change even when a live view or recording/replay is + active. Active recordings will be gracefully stopped and restarted at the new + resolution as separate segments. If false (default), the resize is refused when + a live view or recording is active. + """ diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index 2addbb87..1e612ff2 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -158,6 +158,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None: "height": 800, "width": 1280, "refresh_rate": 60, + "force": True, }, ) assert_matches_type(BrowserUpdateResponse, browser, path=["response"]) @@ -521,6 +522,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> "height": 800, "width": 1280, "refresh_rate": 60, + "force": True, }, ) assert_matches_type(BrowserUpdateResponse, browser, path=["response"]) From 88b11b46bce1f2bd1eb4822852f38cf3c2a0fcac Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 16:05:01 +0000 Subject: [PATCH 330/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 52afe059..dc28bb87 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.42.0" + ".": "0.42.1" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 968ba8d8..225ad6fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.42.0" +version = "0.42.1" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 7a26ebd7..e6149c73 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.42.0" # x-release-please-version +__version__ = "0.42.1" # x-release-please-version From 336f076bc379c17100d2f2168fc1a96a347562a3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 15:44:42 +0000 Subject: [PATCH 331/448] chore(ci): skip uploading artifacts on stainless-internal branches --- .github/workflows/ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b438aafe..5f78a6a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,14 +61,18 @@ jobs: run: rye build - name: Get GitHub OIDC Token - if: github.repository == 'stainless-sdks/kernel-python' + if: |- + github.repository == 'stainless-sdks/kernel-python' && + !startsWith(github.ref, 'refs/heads/stl/') id: github-oidc uses: actions/github-script@v8 with: script: core.setOutput('github_token', await core.getIDToken()); - name: Upload tarball - if: github.repository == 'stainless-sdks/kernel-python' + if: |- + github.repository == 'stainless-sdks/kernel-python' && + !startsWith(github.ref, 'refs/heads/stl/') env: URL: https://pkg.stainless.com/s AUTH: ${{ steps.github-oidc.outputs.github_token }} From bf6685fe39a5d8d6747d990d7f58ac813c82e2e4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 15:46:23 +0000 Subject: [PATCH 332/448] chore: update placeholder string --- tests/api_resources/browsers/test_fs.py | 52 ++++++++++++------------- tests/api_resources/test_browsers.py | 16 ++++---- tests/api_resources/test_deployments.py | 4 +- tests/api_resources/test_extensions.py | 16 ++++---- 4 files changed, 44 insertions(+), 44 deletions(-) diff --git a/tests/api_resources/browsers/test_fs.py b/tests/api_resources/browsers/test_fs.py index 1d314e3a..ceb179a0 100644 --- a/tests/api_resources/browsers/test_fs.py +++ b/tests/api_resources/browsers/test_fs.py @@ -496,7 +496,7 @@ def test_method_upload(self, client: Kernel) -> None: files=[ { "dest_path": "/J!", - "file": b"raw file contents", + "file": b"Example data", } ], ) @@ -510,7 +510,7 @@ def test_raw_response_upload(self, client: Kernel) -> None: files=[ { "dest_path": "/J!", - "file": b"raw file contents", + "file": b"Example data", } ], ) @@ -528,7 +528,7 @@ def test_streaming_response_upload(self, client: Kernel) -> None: files=[ { "dest_path": "/J!", - "file": b"raw file contents", + "file": b"Example data", } ], ) as response: @@ -549,7 +549,7 @@ def test_path_params_upload(self, client: Kernel) -> None: files=[ { "dest_path": "/J!", - "file": b"raw file contents", + "file": b"Example data", } ], ) @@ -560,7 +560,7 @@ def test_method_upload_zip(self, client: Kernel) -> None: f = client.browsers.fs.upload_zip( id="id", dest_path="/J!", - zip_file=b"raw file contents", + zip_file=b"Example data", ) assert f is None @@ -570,7 +570,7 @@ def test_raw_response_upload_zip(self, client: Kernel) -> None: response = client.browsers.fs.with_raw_response.upload_zip( id="id", dest_path="/J!", - zip_file=b"raw file contents", + zip_file=b"Example data", ) assert response.is_closed is True @@ -584,7 +584,7 @@ def test_streaming_response_upload_zip(self, client: Kernel) -> None: with client.browsers.fs.with_streaming_response.upload_zip( id="id", dest_path="/J!", - zip_file=b"raw file contents", + zip_file=b"Example data", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -601,7 +601,7 @@ def test_path_params_upload_zip(self, client: Kernel) -> None: client.browsers.fs.with_raw_response.upload_zip( id="", dest_path="/J!", - zip_file=b"raw file contents", + zip_file=b"Example data", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -609,7 +609,7 @@ def test_path_params_upload_zip(self, client: Kernel) -> None: def test_method_write_file(self, client: Kernel) -> None: f = client.browsers.fs.write_file( id="id", - contents=b"raw file contents", + contents=b"Example data", path="/J!", ) assert f is None @@ -619,7 +619,7 @@ def test_method_write_file(self, client: Kernel) -> None: def test_method_write_file_with_all_params(self, client: Kernel) -> None: f = client.browsers.fs.write_file( id="id", - contents=b"raw file contents", + contents=b"Example data", path="/J!", mode="0611", ) @@ -630,7 +630,7 @@ def test_method_write_file_with_all_params(self, client: Kernel) -> None: def test_raw_response_write_file(self, client: Kernel) -> None: response = client.browsers.fs.with_raw_response.write_file( id="id", - contents=b"raw file contents", + contents=b"Example data", path="/J!", ) @@ -644,7 +644,7 @@ def test_raw_response_write_file(self, client: Kernel) -> None: def test_streaming_response_write_file(self, client: Kernel) -> None: with client.browsers.fs.with_streaming_response.write_file( id="id", - contents=b"raw file contents", + contents=b"Example data", path="/J!", ) as response: assert not response.is_closed @@ -661,7 +661,7 @@ def test_path_params_write_file(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): client.browsers.fs.with_raw_response.write_file( id="", - contents=b"raw file contents", + contents=b"Example data", path="/J!", ) @@ -1139,7 +1139,7 @@ async def test_method_upload(self, async_client: AsyncKernel) -> None: files=[ { "dest_path": "/J!", - "file": b"raw file contents", + "file": b"Example data", } ], ) @@ -1153,7 +1153,7 @@ async def test_raw_response_upload(self, async_client: AsyncKernel) -> None: files=[ { "dest_path": "/J!", - "file": b"raw file contents", + "file": b"Example data", } ], ) @@ -1171,7 +1171,7 @@ async def test_streaming_response_upload(self, async_client: AsyncKernel) -> Non files=[ { "dest_path": "/J!", - "file": b"raw file contents", + "file": b"Example data", } ], ) as response: @@ -1192,7 +1192,7 @@ async def test_path_params_upload(self, async_client: AsyncKernel) -> None: files=[ { "dest_path": "/J!", - "file": b"raw file contents", + "file": b"Example data", } ], ) @@ -1203,7 +1203,7 @@ async def test_method_upload_zip(self, async_client: AsyncKernel) -> None: f = await async_client.browsers.fs.upload_zip( id="id", dest_path="/J!", - zip_file=b"raw file contents", + zip_file=b"Example data", ) assert f is None @@ -1213,7 +1213,7 @@ async def test_raw_response_upload_zip(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.fs.with_raw_response.upload_zip( id="id", dest_path="/J!", - zip_file=b"raw file contents", + zip_file=b"Example data", ) assert response.is_closed is True @@ -1227,7 +1227,7 @@ async def test_streaming_response_upload_zip(self, async_client: AsyncKernel) -> async with async_client.browsers.fs.with_streaming_response.upload_zip( id="id", dest_path="/J!", - zip_file=b"raw file contents", + zip_file=b"Example data", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -1244,7 +1244,7 @@ async def test_path_params_upload_zip(self, async_client: AsyncKernel) -> None: await async_client.browsers.fs.with_raw_response.upload_zip( id="", dest_path="/J!", - zip_file=b"raw file contents", + zip_file=b"Example data", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -1252,7 +1252,7 @@ async def test_path_params_upload_zip(self, async_client: AsyncKernel) -> None: async def test_method_write_file(self, async_client: AsyncKernel) -> None: f = await async_client.browsers.fs.write_file( id="id", - contents=b"raw file contents", + contents=b"Example data", path="/J!", ) assert f is None @@ -1262,7 +1262,7 @@ async def test_method_write_file(self, async_client: AsyncKernel) -> None: async def test_method_write_file_with_all_params(self, async_client: AsyncKernel) -> None: f = await async_client.browsers.fs.write_file( id="id", - contents=b"raw file contents", + contents=b"Example data", path="/J!", mode="0611", ) @@ -1273,7 +1273,7 @@ async def test_method_write_file_with_all_params(self, async_client: AsyncKernel async def test_raw_response_write_file(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.fs.with_raw_response.write_file( id="id", - contents=b"raw file contents", + contents=b"Example data", path="/J!", ) @@ -1287,7 +1287,7 @@ async def test_raw_response_write_file(self, async_client: AsyncKernel) -> None: async def test_streaming_response_write_file(self, async_client: AsyncKernel) -> None: async with async_client.browsers.fs.with_streaming_response.write_file( id="id", - contents=b"raw file contents", + contents=b"Example data", path="/J!", ) as response: assert not response.is_closed @@ -1304,6 +1304,6 @@ async def test_path_params_write_file(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): await async_client.browsers.fs.with_raw_response.write_file( id="", - contents=b"raw file contents", + contents=b"Example data", path="/J!", ) diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index 1e612ff2..39c5bef4 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -325,7 +325,7 @@ def test_method_load_extensions(self, client: Kernel) -> None: extensions=[ { "name": "name", - "zip_file": b"raw file contents", + "zip_file": b"Example data", } ], ) @@ -339,7 +339,7 @@ def test_raw_response_load_extensions(self, client: Kernel) -> None: extensions=[ { "name": "name", - "zip_file": b"raw file contents", + "zip_file": b"Example data", } ], ) @@ -357,7 +357,7 @@ def test_streaming_response_load_extensions(self, client: Kernel) -> None: extensions=[ { "name": "name", - "zip_file": b"raw file contents", + "zip_file": b"Example data", } ], ) as response: @@ -378,7 +378,7 @@ def test_path_params_load_extensions(self, client: Kernel) -> None: extensions=[ { "name": "name", - "zip_file": b"raw file contents", + "zip_file": b"Example data", } ], ) @@ -689,7 +689,7 @@ async def test_method_load_extensions(self, async_client: AsyncKernel) -> None: extensions=[ { "name": "name", - "zip_file": b"raw file contents", + "zip_file": b"Example data", } ], ) @@ -703,7 +703,7 @@ async def test_raw_response_load_extensions(self, async_client: AsyncKernel) -> extensions=[ { "name": "name", - "zip_file": b"raw file contents", + "zip_file": b"Example data", } ], ) @@ -721,7 +721,7 @@ async def test_streaming_response_load_extensions(self, async_client: AsyncKerne extensions=[ { "name": "name", - "zip_file": b"raw file contents", + "zip_file": b"Example data", } ], ) as response: @@ -742,7 +742,7 @@ async def test_path_params_load_extensions(self, async_client: AsyncKernel) -> N extensions=[ { "name": "name", - "zip_file": b"raw file contents", + "zip_file": b"Example data", } ], ) diff --git a/tests/api_resources/test_deployments.py b/tests/api_resources/test_deployments.py index 25ad439e..ed562864 100644 --- a/tests/api_resources/test_deployments.py +++ b/tests/api_resources/test_deployments.py @@ -34,7 +34,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: deployment = client.deployments.create( entrypoint_rel_path="src/app.py", env_vars={"FOO": "bar"}, - file=b"raw file contents", + file=b"Example data", force=False, region="aws.us-east-1a", source={ @@ -265,7 +265,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> deployment = await async_client.deployments.create( entrypoint_rel_path="src/app.py", env_vars={"FOO": "bar"}, - file=b"raw file contents", + file=b"Example data", force=False, region="aws.us-east-1a", source={ diff --git a/tests/api_resources/test_extensions.py b/tests/api_resources/test_extensions.py index 6f31f548..4a28fb5e 100644 --- a/tests/api_resources/test_extensions.py +++ b/tests/api_resources/test_extensions.py @@ -207,7 +207,7 @@ def test_streaming_response_download_from_chrome_store(self, client: Kernel, res @parametrize def test_method_upload(self, client: Kernel) -> None: extension = client.extensions.upload( - file=b"raw file contents", + file=b"Example data", ) assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) @@ -215,7 +215,7 @@ def test_method_upload(self, client: Kernel) -> None: @parametrize def test_method_upload_with_all_params(self, client: Kernel) -> None: extension = client.extensions.upload( - file=b"raw file contents", + file=b"Example data", name="name", ) assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) @@ -224,7 +224,7 @@ def test_method_upload_with_all_params(self, client: Kernel) -> None: @parametrize def test_raw_response_upload(self, client: Kernel) -> None: response = client.extensions.with_raw_response.upload( - file=b"raw file contents", + file=b"Example data", ) assert response.is_closed is True @@ -236,7 +236,7 @@ def test_raw_response_upload(self, client: Kernel) -> None: @parametrize def test_streaming_response_upload(self, client: Kernel) -> None: with client.extensions.with_streaming_response.upload( - file=b"raw file contents", + file=b"Example data", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -437,7 +437,7 @@ async def test_streaming_response_download_from_chrome_store( @parametrize async def test_method_upload(self, async_client: AsyncKernel) -> None: extension = await async_client.extensions.upload( - file=b"raw file contents", + file=b"Example data", ) assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) @@ -445,7 +445,7 @@ async def test_method_upload(self, async_client: AsyncKernel) -> None: @parametrize async def test_method_upload_with_all_params(self, async_client: AsyncKernel) -> None: extension = await async_client.extensions.upload( - file=b"raw file contents", + file=b"Example data", name="name", ) assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) @@ -454,7 +454,7 @@ async def test_method_upload_with_all_params(self, async_client: AsyncKernel) -> @parametrize async def test_raw_response_upload(self, async_client: AsyncKernel) -> None: response = await async_client.extensions.with_raw_response.upload( - file=b"raw file contents", + file=b"Example data", ) assert response.is_closed is True @@ -466,7 +466,7 @@ async def test_raw_response_upload(self, async_client: AsyncKernel) -> None: @parametrize async def test_streaming_response_upload(self, async_client: AsyncKernel) -> None: async with async_client.extensions.with_streaming_response.upload( - file=b"raw file contents", + file=b"Example data", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" From 8de99f17ab1bb36c8d516da0ad62b972095046af Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 16:05:54 +0000 Subject: [PATCH 333/448] feat: Add webdriver_ws_url and metro webdriver session proxy --- .stats.yml | 4 ++-- src/kernel/types/browser_create_response.py | 3 +++ src/kernel/types/browser_list_response.py | 3 +++ src/kernel/types/browser_pool_acquire_response.py | 3 +++ src/kernel/types/browser_retrieve_response.py | 3 +++ src/kernel/types/browser_update_response.py | 3 +++ src/kernel/types/invocation_list_browsers_response.py | 3 +++ 7 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index e1ce185b..ae22a711 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 103 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-ef24d4bf172555bcbe8e3b432c644a25a1c6afd99c958a2eda8c3b1ea9568113.yml -openapi_spec_hash: b603c5a983e837928fa7d1100ed64fc9 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-bda5e58fa0bbd08761f27a1e0edbc602c44141ac9483bf6c96d52b7f4d10d9a7.yml +openapi_spec_hash: 10833b36358e8cda023e5bb0abeab0ba config_hash: cff4d43372b6fa66b64e2d4150f6aa76 diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index 646e25aa..bbfd9a22 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -32,6 +32,9 @@ class BrowserCreateResponse(BaseModel): timeout_seconds: int """The number of seconds of inactivity before the browser session is terminated.""" + webdriver_ws_url: str + """Websocket URL for WebDriver BiDi connections to the browser session""" + browser_live_view_url: Optional[str] = None """Remote URL for live viewing the browser session. diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index 7bb748fb..915df11c 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -32,6 +32,9 @@ class BrowserListResponse(BaseModel): timeout_seconds: int """The number of seconds of inactivity before the browser session is terminated.""" + webdriver_ws_url: str + """Websocket URL for WebDriver BiDi connections to the browser session""" + browser_live_view_url: Optional[str] = None """Remote URL for live viewing the browser session. diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py index 3ffb7777..373274c4 100644 --- a/src/kernel/types/browser_pool_acquire_response.py +++ b/src/kernel/types/browser_pool_acquire_response.py @@ -32,6 +32,9 @@ class BrowserPoolAcquireResponse(BaseModel): timeout_seconds: int """The number of seconds of inactivity before the browser session is terminated.""" + webdriver_ws_url: str + """Websocket URL for WebDriver BiDi connections to the browser session""" + browser_live_view_url: Optional[str] = None """Remote URL for live viewing the browser session. diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 3a5df2ca..76fc6ce1 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -32,6 +32,9 @@ class BrowserRetrieveResponse(BaseModel): timeout_seconds: int """The number of seconds of inactivity before the browser session is terminated.""" + webdriver_ws_url: str + """Websocket URL for WebDriver BiDi connections to the browser session""" + browser_live_view_url: Optional[str] = None """Remote URL for live viewing the browser session. diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py index 309a7f46..fdf4fb51 100644 --- a/src/kernel/types/browser_update_response.py +++ b/src/kernel/types/browser_update_response.py @@ -32,6 +32,9 @@ class BrowserUpdateResponse(BaseModel): timeout_seconds: int """The number of seconds of inactivity before the browser session is terminated.""" + webdriver_ws_url: str + """Websocket URL for WebDriver BiDi connections to the browser session""" + browser_live_view_url: Optional[str] = None """Remote URL for live viewing the browser session. diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py index cb98f332..673615b2 100644 --- a/src/kernel/types/invocation_list_browsers_response.py +++ b/src/kernel/types/invocation_list_browsers_response.py @@ -32,6 +32,9 @@ class Browser(BaseModel): timeout_seconds: int """The number of seconds of inactivity before the browser session is terminated.""" + webdriver_ws_url: str + """Websocket URL for WebDriver BiDi connections to the browser session""" + browser_live_view_url: Optional[str] = None """Remote URL for live viewing the browser session. From bdc03b15c4982bdb39550b12b1c12c71b60df801 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 17:30:41 +0000 Subject: [PATCH 334/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index dc28bb87..fe87cd91 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.42.1" + ".": "0.43.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 225ad6fb..021d31eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.42.1" +version = "0.43.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index e6149c73..068f52c7 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.42.1" # x-release-please-version +__version__ = "0.43.0" # x-release-please-version From 6636dcd722abde00b7d049c22f13ad05cbc2bbcd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 22:02:41 +0000 Subject: [PATCH 335/448] feat: expose smooth drag mouse movement via public API --- .stats.yml | 4 ++-- src/kernel/resources/browsers/computer.py | 20 +++++++++++++++++++ .../types/browsers/computer_batch_params.py | 12 +++++++++++ .../browsers/computer_drag_mouse_params.py | 12 +++++++++++ tests/api_resources/browsers/test_computer.py | 4 ++++ 5 files changed, 50 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index ae22a711..81407436 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 103 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-bda5e58fa0bbd08761f27a1e0edbc602c44141ac9483bf6c96d52b7f4d10d9a7.yml -openapi_spec_hash: 10833b36358e8cda023e5bb0abeab0ba +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-e6936890166ce5b11abaccd511a43a8807e2abf96c1f542d4f8d94655ef27d1f.yml +openapi_spec_hash: 0146ecaea96d8136ef4a35cd04aacf22 config_hash: cff4d43372b6fa66b64e2d4150f6aa76 diff --git a/src/kernel/resources/browsers/computer.py b/src/kernel/resources/browsers/computer.py index 1357c1e8..bcde24e5 100644 --- a/src/kernel/resources/browsers/computer.py +++ b/src/kernel/resources/browsers/computer.py @@ -213,7 +213,9 @@ def drag_mouse( path: Iterable[Iterable[int]], button: Literal["left", "middle", "right"] | Omit = omit, delay: int | Omit = omit, + duration_ms: int | Omit = omit, hold_keys: SequenceNotStr[str] | Omit = omit, + smooth: bool | Omit = omit, step_delay_ms: int | Omit = omit, steps_per_segment: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -234,8 +236,14 @@ def drag_mouse( delay: Delay in milliseconds between button down and starting to move along the path. + duration_ms: Target total duration in milliseconds for the entire drag movement when + smooth=true. Omit for automatic timing based on total path length. + hold_keys: Modifier keys to hold during the drag + smooth: Use human-like Bezier curves between path waypoints instead of linear + interpolation. When true, steps_per_segment and step_delay_ms are ignored. + step_delay_ms: Delay in milliseconds between relative steps while dragging (not the initial delay). @@ -259,7 +267,9 @@ def drag_mouse( "path": path, "button": button, "delay": delay, + "duration_ms": duration_ms, "hold_keys": hold_keys, + "smooth": smooth, "step_delay_ms": step_delay_ms, "steps_per_segment": steps_per_segment, }, @@ -804,7 +814,9 @@ async def drag_mouse( path: Iterable[Iterable[int]], button: Literal["left", "middle", "right"] | Omit = omit, delay: int | Omit = omit, + duration_ms: int | Omit = omit, hold_keys: SequenceNotStr[str] | Omit = omit, + smooth: bool | Omit = omit, step_delay_ms: int | Omit = omit, steps_per_segment: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -825,8 +837,14 @@ async def drag_mouse( delay: Delay in milliseconds between button down and starting to move along the path. + duration_ms: Target total duration in milliseconds for the entire drag movement when + smooth=true. Omit for automatic timing based on total path length. + hold_keys: Modifier keys to hold during the drag + smooth: Use human-like Bezier curves between path waypoints instead of linear + interpolation. When true, steps_per_segment and step_delay_ms are ignored. + step_delay_ms: Delay in milliseconds between relative steps while dragging (not the initial delay). @@ -850,7 +868,9 @@ async def drag_mouse( "path": path, "button": button, "delay": delay, + "duration_ms": duration_ms, "hold_keys": hold_keys, + "smooth": smooth, "step_delay_ms": step_delay_ms, "steps_per_segment": steps_per_segment, }, diff --git a/src/kernel/types/browsers/computer_batch_params.py b/src/kernel/types/browsers/computer_batch_params.py index 9aca7244..db68466d 100644 --- a/src/kernel/types/browsers/computer_batch_params.py +++ b/src/kernel/types/browsers/computer_batch_params.py @@ -59,9 +59,21 @@ class ActionDragMouse(TypedDict, total=False): delay: int """Delay in milliseconds between button down and starting to move along the path.""" + duration_ms: int + """ + Target total duration in milliseconds for the entire drag movement when + smooth=true. Omit for automatic timing based on total path length. + """ + hold_keys: SequenceNotStr[str] """Modifier keys to hold during the drag""" + smooth: bool + """ + Use human-like Bezier curves between path waypoints instead of linear + interpolation. When true, steps_per_segment and step_delay_ms are ignored. + """ + step_delay_ms: int """ Delay in milliseconds between relative steps while dragging (not the initial diff --git a/src/kernel/types/browsers/computer_drag_mouse_params.py b/src/kernel/types/browsers/computer_drag_mouse_params.py index fb03b4be..c0dd4c8e 100644 --- a/src/kernel/types/browsers/computer_drag_mouse_params.py +++ b/src/kernel/types/browsers/computer_drag_mouse_params.py @@ -23,9 +23,21 @@ class ComputerDragMouseParams(TypedDict, total=False): delay: int """Delay in milliseconds between button down and starting to move along the path.""" + duration_ms: int + """ + Target total duration in milliseconds for the entire drag movement when + smooth=true. Omit for automatic timing based on total path length. + """ + hold_keys: SequenceNotStr[str] """Modifier keys to hold during the drag""" + smooth: bool + """ + Use human-like Bezier curves between path waypoints instead of linear + interpolation. When true, steps_per_segment and step_delay_ms are ignored. + """ + step_delay_ms: int """ Delay in milliseconds between relative steps while dragging (not the initial diff --git a/tests/api_resources/browsers/test_computer.py b/tests/api_resources/browsers/test_computer.py index 09960bfc..31974d5b 100644 --- a/tests/api_resources/browsers/test_computer.py +++ b/tests/api_resources/browsers/test_computer.py @@ -224,7 +224,9 @@ def test_method_drag_mouse_with_all_params(self, client: Kernel) -> None: path=[[0, 0], [0, 0]], button="left", delay=0, + duration_ms=50, hold_keys=["string"], + smooth=True, step_delay_ms=0, steps_per_segment=1, ) @@ -887,7 +889,9 @@ async def test_method_drag_mouse_with_all_params(self, async_client: AsyncKernel path=[[0, 0], [0, 0]], button="left", delay=0, + duration_ms=50, hold_keys=["string"], + smooth=True, step_delay_ms=0, steps_per_segment=1, ) From 005ac5dff2f3476dadf1191787d7e12e8e289713 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 20:04:37 +0000 Subject: [PATCH 336/448] feat: Add GPU viewport presets and GPU encoder defaults --- .stats.yml | 4 +- src/kernel/resources/browser_pools.py | 40 +++++++++++++------ src/kernel/resources/browsers/browsers.py | 20 +++++++--- src/kernel/types/browser_create_params.py | 22 +++++----- src/kernel/types/browser_create_response.py | 22 +++++----- src/kernel/types/browser_list_response.py | 22 +++++----- src/kernel/types/browser_pool.py | 22 +++++----- .../types/browser_pool_acquire_response.py | 22 +++++----- .../types/browser_pool_create_params.py | 22 +++++----- .../types/browser_pool_update_params.py | 22 +++++----- src/kernel/types/browser_retrieve_response.py | 22 +++++----- src/kernel/types/browser_update_response.py | 22 +++++----- .../invocation_list_browsers_response.py | 22 +++++----- src/kernel/types/shared/browser_viewport.py | 10 +++-- .../types/shared_params/browser_viewport.py | 10 +++-- 15 files changed, 188 insertions(+), 116 deletions(-) diff --git a/.stats.yml b/.stats.yml index 81407436..904cb96c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 103 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-e6936890166ce5b11abaccd511a43a8807e2abf96c1f542d4f8d94655ef27d1f.yml -openapi_spec_hash: 0146ecaea96d8136ef4a35cd04aacf22 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-95bb1cbe27cbed0339067fa133590e675b99cda4a9c04fad802a5b14563eb572.yml +openapi_spec_hash: 3a24e61711eedb9ea7bb7589a7df956f config_hash: cff4d43372b6fa66b64e2d4150f6aa76 diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py index a5b6e59a..ea45cd13 100644 --- a/src/kernel/resources/browser_pools.py +++ b/src/kernel/resources/browser_pools.py @@ -110,9 +110,13 @@ def create( are destroyed. Defaults to 600 seconds if not specified viewport: Initial browser window size in pixels with optional refresh rate. If omitted, - image defaults apply (1920x1080@25). Arbitrary viewport dimensions are accepted, - but the following configurations are known-good and fully tested: 2560x1440@10, - 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. + image defaults apply (1920x1080@25). For GPU images, the default is + 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended + presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, + 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. Viewports outside this list may exhibit unstable live view or recording behavior. If refresh_rate is not provided, it will be automatically determined based on the resolution (higher resolutions use lower refresh rates to keep @@ -242,9 +246,13 @@ def update( are destroyed. Defaults to 600 seconds if not specified viewport: Initial browser window size in pixels with optional refresh rate. If omitted, - image defaults apply (1920x1080@25). Arbitrary viewport dimensions are accepted, - but the following configurations are known-good and fully tested: 2560x1440@10, - 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. + image defaults apply (1920x1080@25). For GPU images, the default is + 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended + presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, + 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. Viewports outside this list may exhibit unstable live view or recording behavior. If refresh_rate is not provided, it will be automatically determined based on the resolution (higher resolutions use lower refresh rates to keep @@ -550,9 +558,13 @@ async def create( are destroyed. Defaults to 600 seconds if not specified viewport: Initial browser window size in pixels with optional refresh rate. If omitted, - image defaults apply (1920x1080@25). Arbitrary viewport dimensions are accepted, - but the following configurations are known-good and fully tested: 2560x1440@10, - 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. + image defaults apply (1920x1080@25). For GPU images, the default is + 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended + presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, + 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. Viewports outside this list may exhibit unstable live view or recording behavior. If refresh_rate is not provided, it will be automatically determined based on the resolution (higher resolutions use lower refresh rates to keep @@ -682,9 +694,13 @@ async def update( are destroyed. Defaults to 600 seconds if not specified viewport: Initial browser window size in pixels with optional refresh rate. If omitted, - image defaults apply (1920x1080@25). Arbitrary viewport dimensions are accepted, - but the following configurations are known-good and fully tested: 2560x1440@10, - 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. + image defaults apply (1920x1080@25). For GPU images, the default is + 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended + presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, + 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. Viewports outside this list may exhibit unstable live view or recording behavior. If refresh_rate is not provided, it will be automatically determined based on the resolution (higher resolutions use lower refresh rates to keep diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 235da236..e12b2842 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -196,9 +196,13 @@ def create( see is +/- 5 seconds around the specified value. viewport: Initial browser window size in pixels with optional refresh rate. If omitted, - image defaults apply (1920x1080@25). Arbitrary viewport dimensions are accepted, - but the following configurations are known-good and fully tested: 2560x1440@10, - 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. + image defaults apply (1920x1080@25). For GPU images, the default is + 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended + presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, + 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. Viewports outside this list may exhibit unstable live view or recording behavior. If refresh_rate is not provided, it will be automatically determined based on the resolution (higher resolutions use lower refresh rates to keep @@ -623,9 +627,13 @@ async def create( see is +/- 5 seconds around the specified value. viewport: Initial browser window size in pixels with optional refresh rate. If omitted, - image defaults apply (1920x1080@25). Arbitrary viewport dimensions are accepted, - but the following configurations are known-good and fully tested: 2560x1440@10, - 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. + image defaults apply (1920x1080@25). For GPU images, the default is + 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended + presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, + 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. Viewports outside this list may exhibit unstable live view or recording behavior. If refresh_rate is not provided, it will be automatically determined based on the resolution (higher resolutions use lower refresh rates to keep diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index 6df24637..fd22ee76 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -73,13 +73,17 @@ class BrowserCreateParams(TypedDict, total=False): """ viewport: BrowserViewport - """Initial browser window size in pixels with optional refresh rate. - - If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions - are accepted, but the following configurations are known-good and fully tested: - 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, - 1200x800@60. Viewports outside this list may exhibit unstable live view or - recording behavior. If refresh_rate is not provided, it will be automatically - determined based on the resolution (higher resolutions use lower refresh rates - to keep bandwidth reasonable). + """ + Initial browser window size in pixels with optional refresh rate. If omitted, + image defaults apply (1920x1080@25). For GPU images, the default is + 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended + presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, + 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. + Viewports outside this list may exhibit unstable live view or recording + behavior. If refresh_rate is not provided, it will be automatically determined + based on the resolution (higher resolutions use lower refresh rates to keep + bandwidth reasonable). """ diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index bbfd9a22..4b567659 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -66,13 +66,17 @@ class BrowserCreateResponse(BaseModel): """Session usage metrics.""" viewport: Optional[BrowserViewport] = None - """Initial browser window size in pixels with optional refresh rate. - - If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions - are accepted, but the following configurations are known-good and fully tested: - 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, - 1200x800@60. Viewports outside this list may exhibit unstable live view or - recording behavior. If refresh_rate is not provided, it will be automatically - determined based on the resolution (higher resolutions use lower refresh rates - to keep bandwidth reasonable). + """ + Initial browser window size in pixels with optional refresh rate. If omitted, + image defaults apply (1920x1080@25). For GPU images, the default is + 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended + presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, + 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. + Viewports outside this list may exhibit unstable live view or recording + behavior. If refresh_rate is not provided, it will be automatically determined + based on the resolution (higher resolutions use lower refresh rates to keep + bandwidth reasonable). """ diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index 915df11c..c6d97527 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -66,13 +66,17 @@ class BrowserListResponse(BaseModel): """Session usage metrics.""" viewport: Optional[BrowserViewport] = None - """Initial browser window size in pixels with optional refresh rate. - - If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions - are accepted, but the following configurations are known-good and fully tested: - 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, - 1200x800@60. Viewports outside this list may exhibit unstable live view or - recording behavior. If refresh_rate is not provided, it will be automatically - determined based on the resolution (higher resolutions use lower refresh rates - to keep bandwidth reasonable). + """ + Initial browser window size in pixels with optional refresh rate. If omitted, + image defaults apply (1920x1080@25). For GPU images, the default is + 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended + presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, + 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. + Viewports outside this list may exhibit unstable live view or recording + behavior. If refresh_rate is not provided, it will be automatically determined + based on the resolution (higher resolutions use lower refresh rates to keep + bandwidth reasonable). """ diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py index fc4e0f1d..c6286acc 100644 --- a/src/kernel/types/browser_pool.py +++ b/src/kernel/types/browser_pool.py @@ -68,15 +68,19 @@ class BrowserPoolConfig(BaseModel): """ viewport: Optional[BrowserViewport] = None - """Initial browser window size in pixels with optional refresh rate. - - If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions - are accepted, but the following configurations are known-good and fully tested: - 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, - 1200x800@60. Viewports outside this list may exhibit unstable live view or - recording behavior. If refresh_rate is not provided, it will be automatically - determined based on the resolution (higher resolutions use lower refresh rates - to keep bandwidth reasonable). + """ + Initial browser window size in pixels with optional refresh rate. If omitted, + image defaults apply (1920x1080@25). For GPU images, the default is + 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended + presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, + 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. + Viewports outside this list may exhibit unstable live view or recording + behavior. If refresh_rate is not provided, it will be automatically determined + based on the resolution (higher resolutions use lower refresh rates to keep + bandwidth reasonable). """ diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py index 373274c4..29cc6abc 100644 --- a/src/kernel/types/browser_pool_acquire_response.py +++ b/src/kernel/types/browser_pool_acquire_response.py @@ -66,13 +66,17 @@ class BrowserPoolAcquireResponse(BaseModel): """Session usage metrics.""" viewport: Optional[BrowserViewport] = None - """Initial browser window size in pixels with optional refresh rate. - - If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions - are accepted, but the following configurations are known-good and fully tested: - 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, - 1200x800@60. Viewports outside this list may exhibit unstable live view or - recording behavior. If refresh_rate is not provided, it will be automatically - determined based on the resolution (higher resolutions use lower refresh rates - to keep bandwidth reasonable). + """ + Initial browser window size in pixels with optional refresh rate. If omitted, + image defaults apply (1920x1080@25). For GPU images, the default is + 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended + presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, + 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. + Viewports outside this list may exhibit unstable live view or recording + behavior. If refresh_rate is not provided, it will be automatically determined + based on the resolution (higher resolutions use lower refresh rates to keep + bandwidth reasonable). """ diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py index 78268a50..63ef712b 100644 --- a/src/kernel/types/browser_pool_create_params.py +++ b/src/kernel/types/browser_pool_create_params.py @@ -67,13 +67,17 @@ class BrowserPoolCreateParams(TypedDict, total=False): """ viewport: BrowserViewport - """Initial browser window size in pixels with optional refresh rate. - - If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions - are accepted, but the following configurations are known-good and fully tested: - 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, - 1200x800@60. Viewports outside this list may exhibit unstable live view or - recording behavior. If refresh_rate is not provided, it will be automatically - determined based on the resolution (higher resolutions use lower refresh rates - to keep bandwidth reasonable). + """ + Initial browser window size in pixels with optional refresh rate. If omitted, + image defaults apply (1920x1080@25). For GPU images, the default is + 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended + presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, + 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. + Viewports outside this list may exhibit unstable live view or recording + behavior. If refresh_rate is not provided, it will be automatically determined + based on the resolution (higher resolutions use lower refresh rates to keep + bandwidth reasonable). """ diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py index 74b76a63..d1f003b5 100644 --- a/src/kernel/types/browser_pool_update_params.py +++ b/src/kernel/types/browser_pool_update_params.py @@ -73,13 +73,17 @@ class BrowserPoolUpdateParams(TypedDict, total=False): """ viewport: BrowserViewport - """Initial browser window size in pixels with optional refresh rate. - - If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions - are accepted, but the following configurations are known-good and fully tested: - 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, - 1200x800@60. Viewports outside this list may exhibit unstable live view or - recording behavior. If refresh_rate is not provided, it will be automatically - determined based on the resolution (higher resolutions use lower refresh rates - to keep bandwidth reasonable). + """ + Initial browser window size in pixels with optional refresh rate. If omitted, + image defaults apply (1920x1080@25). For GPU images, the default is + 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended + presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, + 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. + Viewports outside this list may exhibit unstable live view or recording + behavior. If refresh_rate is not provided, it will be automatically determined + based on the resolution (higher resolutions use lower refresh rates to keep + bandwidth reasonable). """ diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 76fc6ce1..9355af9a 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -66,13 +66,17 @@ class BrowserRetrieveResponse(BaseModel): """Session usage metrics.""" viewport: Optional[BrowserViewport] = None - """Initial browser window size in pixels with optional refresh rate. - - If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions - are accepted, but the following configurations are known-good and fully tested: - 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, - 1200x800@60. Viewports outside this list may exhibit unstable live view or - recording behavior. If refresh_rate is not provided, it will be automatically - determined based on the resolution (higher resolutions use lower refresh rates - to keep bandwidth reasonable). + """ + Initial browser window size in pixels with optional refresh rate. If omitted, + image defaults apply (1920x1080@25). For GPU images, the default is + 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended + presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, + 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. + Viewports outside this list may exhibit unstable live view or recording + behavior. If refresh_rate is not provided, it will be automatically determined + based on the resolution (higher resolutions use lower refresh rates to keep + bandwidth reasonable). """ diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py index fdf4fb51..3bf53e70 100644 --- a/src/kernel/types/browser_update_response.py +++ b/src/kernel/types/browser_update_response.py @@ -66,13 +66,17 @@ class BrowserUpdateResponse(BaseModel): """Session usage metrics.""" viewport: Optional[BrowserViewport] = None - """Initial browser window size in pixels with optional refresh rate. - - If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions - are accepted, but the following configurations are known-good and fully tested: - 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, - 1200x800@60. Viewports outside this list may exhibit unstable live view or - recording behavior. If refresh_rate is not provided, it will be automatically - determined based on the resolution (higher resolutions use lower refresh rates - to keep bandwidth reasonable). + """ + Initial browser window size in pixels with optional refresh rate. If omitted, + image defaults apply (1920x1080@25). For GPU images, the default is + 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended + presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, + 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. + Viewports outside this list may exhibit unstable live view or recording + behavior. If refresh_rate is not provided, it will be automatically determined + based on the resolution (higher resolutions use lower refresh rates to keep + bandwidth reasonable). """ diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py index 673615b2..0c6451ca 100644 --- a/src/kernel/types/invocation_list_browsers_response.py +++ b/src/kernel/types/invocation_list_browsers_response.py @@ -66,15 +66,19 @@ class Browser(BaseModel): """Session usage metrics.""" viewport: Optional[BrowserViewport] = None - """Initial browser window size in pixels with optional refresh rate. - - If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions - are accepted, but the following configurations are known-good and fully tested: - 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, - 1200x800@60. Viewports outside this list may exhibit unstable live view or - recording behavior. If refresh_rate is not provided, it will be automatically - determined based on the resolution (higher resolutions use lower refresh rates - to keep bandwidth reasonable). + """ + Initial browser window size in pixels with optional refresh rate. If omitted, + image defaults apply (1920x1080@25). For GPU images, the default is + 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended + presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, + 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. + Viewports outside this list may exhibit unstable live view or recording + behavior. If refresh_rate is not provided, it will be automatically determined + based on the resolution (higher resolutions use lower refresh rates to keep + bandwidth reasonable). """ diff --git a/src/kernel/types/shared/browser_viewport.py b/src/kernel/types/shared/browser_viewport.py index dacac1f2..c53505be 100644 --- a/src/kernel/types/shared/browser_viewport.py +++ b/src/kernel/types/shared/browser_viewport.py @@ -8,11 +8,15 @@ class BrowserViewport(BaseModel): - """Initial browser window size in pixels with optional refresh rate. - + """ + Initial browser window size in pixels with optional refresh rate. If omitted, image defaults apply (1920x1080@25). - Arbitrary viewport dimensions are accepted, but the following configurations are known-good and fully tested: + For GPU images, the default is 1920x1080@60. + Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. + For GPU images, recommended presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. Viewports outside this list may exhibit unstable live view or recording behavior. If refresh_rate is not provided, it will be automatically determined based on the resolution (higher resolutions use lower refresh rates to keep bandwidth reasonable). diff --git a/src/kernel/types/shared_params/browser_viewport.py b/src/kernel/types/shared_params/browser_viewport.py index f98ece82..2290f930 100644 --- a/src/kernel/types/shared_params/browser_viewport.py +++ b/src/kernel/types/shared_params/browser_viewport.py @@ -8,11 +8,15 @@ class BrowserViewport(TypedDict, total=False): - """Initial browser window size in pixels with optional refresh rate. - + """ + Initial browser window size in pixels with optional refresh rate. If omitted, image defaults apply (1920x1080@25). - Arbitrary viewport dimensions are accepted, but the following configurations are known-good and fully tested: + For GPU images, the default is 1920x1080@60. + Arbitrary viewport dimensions and refresh rates are accepted. + Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. + For GPU images, recommended presets use one of these resolutions with refresh rates 60, 30, 25, or 10: + 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600. Viewports outside this list may exhibit unstable live view or recording behavior. If refresh_rate is not provided, it will be automatically determined based on the resolution (higher resolutions use lower refresh rates to keep bandwidth reasonable). From 4038075dcaf59d0f2bc43253240a7e0048d971fe Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 13 Mar 2026 13:26:07 +0000 Subject: [PATCH 337/448] feat: Adds description to OAS spec for docs about delta_x, delta_y --- .stats.yml | 4 ++-- src/kernel/resources/browsers/computer.py | 12 ++++++++---- src/kernel/types/browsers/computer_batch_params.py | 10 ++++++++-- src/kernel/types/browsers/computer_scroll_params.py | 10 ++++++++-- 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/.stats.yml b/.stats.yml index 904cb96c..c129c7d3 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 103 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-95bb1cbe27cbed0339067fa133590e675b99cda4a9c04fad802a5b14563eb572.yml -openapi_spec_hash: 3a24e61711eedb9ea7bb7589a7df956f +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-aa981bcc44bf8382844c53b705f75eeac53fdc7cd828a9260755c5b4537ed966.yml +openapi_spec_hash: e78521a8956dc87b25c076e30600a95e config_hash: cff4d43372b6fa66b64e2d4150f6aa76 diff --git a/src/kernel/resources/browsers/computer.py b/src/kernel/resources/browsers/computer.py index bcde24e5..116a7037 100644 --- a/src/kernel/resources/browsers/computer.py +++ b/src/kernel/resources/browsers/computer.py @@ -486,9 +486,11 @@ def scroll( y: Y coordinate at which to perform the scroll - delta_x: Horizontal scroll amount. Positive scrolls right, negative scrolls left. + delta_x: Horizontal scroll amount in xdotool "wheel units." Positive scrolls right, + negative scrolls left. - delta_y: Vertical scroll amount. Positive scrolls down, negative scrolls up. + delta_y: Vertical scroll amount in xdotool "wheel units." Positive scrolls down, negative + scrolls up. hold_keys: Modifier keys to hold during the scroll @@ -1087,9 +1089,11 @@ async def scroll( y: Y coordinate at which to perform the scroll - delta_x: Horizontal scroll amount. Positive scrolls right, negative scrolls left. + delta_x: Horizontal scroll amount in xdotool "wheel units." Positive scrolls right, + negative scrolls left. - delta_y: Vertical scroll amount. Positive scrolls down, negative scrolls up. + delta_y: Vertical scroll amount in xdotool "wheel units." Positive scrolls down, negative + scrolls up. hold_keys: Modifier keys to hold during the scroll diff --git a/src/kernel/types/browsers/computer_batch_params.py b/src/kernel/types/browsers/computer_batch_params.py index db68466d..7fc6abb5 100644 --- a/src/kernel/types/browsers/computer_batch_params.py +++ b/src/kernel/types/browsers/computer_batch_params.py @@ -131,10 +131,16 @@ class ActionScroll(TypedDict, total=False): """Y coordinate at which to perform the scroll""" delta_x: int - """Horizontal scroll amount. Positive scrolls right, negative scrolls left.""" + """ + Horizontal scroll amount in xdotool "wheel units." Positive scrolls right, + negative scrolls left. + """ delta_y: int - """Vertical scroll amount. Positive scrolls down, negative scrolls up.""" + """ + Vertical scroll amount in xdotool "wheel units." Positive scrolls down, negative + scrolls up. + """ hold_keys: SequenceNotStr[str] """Modifier keys to hold during the scroll""" diff --git a/src/kernel/types/browsers/computer_scroll_params.py b/src/kernel/types/browsers/computer_scroll_params.py index 110cb302..3af38af3 100644 --- a/src/kernel/types/browsers/computer_scroll_params.py +++ b/src/kernel/types/browsers/computer_scroll_params.py @@ -17,10 +17,16 @@ class ComputerScrollParams(TypedDict, total=False): """Y coordinate at which to perform the scroll""" delta_x: int - """Horizontal scroll amount. Positive scrolls right, negative scrolls left.""" + """ + Horizontal scroll amount in xdotool "wheel units." Positive scrolls right, + negative scrolls left. + """ delta_y: int - """Vertical scroll amount. Positive scrolls down, negative scrolls up.""" + """ + Vertical scroll amount in xdotool "wheel units." Positive scrolls down, negative + scrolls up. + """ hold_keys: SequenceNotStr[str] """Modifier keys to hold during the scroll""" From 20814772801da87a76c7a10ff92ab1c3e832f149 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 22:33:43 +0000 Subject: [PATCH 338/448] feat: Rename hardware acceleration UI/docs wording to GPU acceleration --- .stats.yml | 4 ++-- src/kernel/resources/browsers/browsers.py | 8 ++++---- src/kernel/types/browser_create_params.py | 2 +- src/kernel/types/browser_create_response.py | 2 +- src/kernel/types/browser_list_response.py | 2 +- src/kernel/types/browser_pool_acquire_response.py | 2 +- src/kernel/types/browser_retrieve_response.py | 2 +- src/kernel/types/browser_update_response.py | 2 +- src/kernel/types/invocation_list_browsers_response.py | 2 +- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.stats.yml b/.stats.yml index c129c7d3..663d1ab4 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 103 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-aa981bcc44bf8382844c53b705f75eeac53fdc7cd828a9260755c5b4537ed966.yml -openapi_spec_hash: e78521a8956dc87b25c076e30600a95e +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-f57c1468805aef5055a41e942a1ec374df98d58f1071b07c31e6496045e0d902.yml +openapi_spec_hash: a4848d54211d6c6330b5ddd08992035a config_hash: cff4d43372b6fa66b64e2d4150f6aa76 diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index e12b2842..a112bdaa 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -166,8 +166,8 @@ def create( Args: extensions: List of browser extensions to load into the session. Provide each by id or name. - gpu: If true, launches a hardware-accelerated browser with GPU rendering. Requires - Start-Up or Enterprise plan. + gpu: If true, enables GPU acceleration for the browser session. Requires Start-Up or + Enterprise plan. headless: If true, launches the browser using a headless image (no VNC/GUI). Defaults to false. @@ -597,8 +597,8 @@ async def create( Args: extensions: List of browser extensions to load into the session. Provide each by id or name. - gpu: If true, launches a hardware-accelerated browser with GPU rendering. Requires - Start-Up or Enterprise plan. + gpu: If true, enables GPU acceleration for the browser session. Requires Start-Up or + Enterprise plan. headless: If true, launches the browser using a headless image (no VNC/GUI). Defaults to false. diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index fd22ee76..f59959e0 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -21,7 +21,7 @@ class BrowserCreateParams(TypedDict, total=False): """ gpu: bool - """If true, launches a hardware-accelerated browser with GPU rendering. + """If true, enables GPU acceleration for the browser session. Requires Start-Up or Enterprise plan. """ diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index 4b567659..f1f8c781 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -45,7 +45,7 @@ class BrowserCreateResponse(BaseModel): """When the browser session was soft-deleted. Only present for deleted sessions.""" gpu: Optional[bool] = None - """Whether the browser session has hardware-accelerated GPU rendering.""" + """Whether GPU acceleration is enabled for the browser session.""" kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index c6d97527..8dbaf1dd 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -45,7 +45,7 @@ class BrowserListResponse(BaseModel): """When the browser session was soft-deleted. Only present for deleted sessions.""" gpu: Optional[bool] = None - """Whether the browser session has hardware-accelerated GPU rendering.""" + """Whether GPU acceleration is enabled for the browser session.""" kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py index 29cc6abc..24e436ef 100644 --- a/src/kernel/types/browser_pool_acquire_response.py +++ b/src/kernel/types/browser_pool_acquire_response.py @@ -45,7 +45,7 @@ class BrowserPoolAcquireResponse(BaseModel): """When the browser session was soft-deleted. Only present for deleted sessions.""" gpu: Optional[bool] = None - """Whether the browser session has hardware-accelerated GPU rendering.""" + """Whether GPU acceleration is enabled for the browser session.""" kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 9355af9a..8b8353f5 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -45,7 +45,7 @@ class BrowserRetrieveResponse(BaseModel): """When the browser session was soft-deleted. Only present for deleted sessions.""" gpu: Optional[bool] = None - """Whether the browser session has hardware-accelerated GPU rendering.""" + """Whether GPU acceleration is enabled for the browser session.""" kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py index 3bf53e70..5b90fa43 100644 --- a/src/kernel/types/browser_update_response.py +++ b/src/kernel/types/browser_update_response.py @@ -45,7 +45,7 @@ class BrowserUpdateResponse(BaseModel): """When the browser session was soft-deleted. Only present for deleted sessions.""" gpu: Optional[bool] = None - """Whether the browser session has hardware-accelerated GPU rendering.""" + """Whether GPU acceleration is enabled for the browser session.""" kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py index 0c6451ca..7e4225ec 100644 --- a/src/kernel/types/invocation_list_browsers_response.py +++ b/src/kernel/types/invocation_list_browsers_response.py @@ -45,7 +45,7 @@ class Browser(BaseModel): """When the browser session was soft-deleted. Only present for deleted sessions.""" gpu: Optional[bool] = None - """Whether the browser session has hardware-accelerated GPU rendering.""" + """Whether GPU acceleration is enabled for the browser session.""" kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" From 3cbc262ef2c5967b29342f4b876e42e6c8f9317f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 04:58:44 +0000 Subject: [PATCH 339/448] fix(pydantic): do not pass `by_alias` unless set --- src/kernel/_compat.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/kernel/_compat.py b/src/kernel/_compat.py index 786ff42a..e6690a4f 100644 --- a/src/kernel/_compat.py +++ b/src/kernel/_compat.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload from datetime import date, datetime -from typing_extensions import Self, Literal +from typing_extensions import Self, Literal, TypedDict import pydantic from pydantic.fields import FieldInfo @@ -131,6 +131,10 @@ def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str: return model.model_dump_json(indent=indent) +class _ModelDumpKwargs(TypedDict, total=False): + by_alias: bool + + def model_dump( model: pydantic.BaseModel, *, @@ -142,6 +146,9 @@ def model_dump( by_alias: bool | None = None, ) -> dict[str, Any]: if (not PYDANTIC_V1) or hasattr(model, "model_dump"): + kwargs: _ModelDumpKwargs = {} + if by_alias is not None: + kwargs["by_alias"] = by_alias return model.model_dump( mode=mode, exclude=exclude, @@ -149,7 +156,7 @@ def model_dump( exclude_defaults=exclude_defaults, # warnings are not supported in Pydantic v1 warnings=True if PYDANTIC_V1 else warnings, - by_alias=by_alias, + **kwargs, ) return cast( "dict[str, Any]", From 2182998ccd8ad2af89471a21c7b8548865fda632 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 05:03:36 +0000 Subject: [PATCH 340/448] fix(deps): bump minimum typing-extensions version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 021d31eb..62a14748 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ authors = [ dependencies = [ "httpx>=0.23.0, <1", "pydantic>=1.9.0, <3", - "typing-extensions>=4.10, <5", + "typing-extensions>=4.14, <5", "anyio>=3.5.0, <5", "distro>=1.7.0, <2", "sniffio", From 2f7eece8da6684607895b2b9d78f2d8e80b309a5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 05:06:36 +0000 Subject: [PATCH 341/448] chore(internal): tweak CI branches --- .github/workflows/ci.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f78a6a1..c7bc99ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,14 @@ name: CI on: push: - branches-ignore: - - 'generated' - - 'codegen/**' - - 'integrated/**' - - 'stl-preview-head/**' - - 'stl-preview-base/**' + branches: + - '**' + - '!integrated/**' + - '!stl-preview-head/**' + - '!stl-preview-base/**' + - '!generated' + - '!codegen/**' + - 'codegen/stl/**' pull_request: branches-ignore: - 'stl-preview-head/**' From 974177907ff01a97f01ea0a367a4bf3ae23635ab Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:14:54 +0000 Subject: [PATCH 342/448] feat: Drop headless GPU support and disable pooling --- .stats.yml | 4 ++-- src/kernel/resources/browsers/browsers.py | 4 ++-- src/kernel/types/browser_create_params.py | 2 +- src/kernel/types/browser_create_response.py | 5 ++++- src/kernel/types/browser_list_response.py | 5 ++++- src/kernel/types/browser_pool_acquire_response.py | 5 ++++- src/kernel/types/browser_retrieve_response.py | 5 ++++- src/kernel/types/browser_update_response.py | 5 ++++- src/kernel/types/invocation_list_browsers_response.py | 5 ++++- 9 files changed, 29 insertions(+), 11 deletions(-) diff --git a/.stats.yml b/.stats.yml index 663d1ab4..ad84265a 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 103 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-f57c1468805aef5055a41e942a1ec374df98d58f1071b07c31e6496045e0d902.yml -openapi_spec_hash: a4848d54211d6c6330b5ddd08992035a +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-17e50cf93d8052ff655c160fc0f156621d9029b041526d4e2e3317b13f80822f.yml +openapi_spec_hash: f7dadc8d93e77983936eb18a8080ce15 config_hash: cff4d43372b6fa66b64e2d4150f6aa76 diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index a112bdaa..1d1ce22a 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -167,7 +167,7 @@ def create( extensions: List of browser extensions to load into the session. Provide each by id or name. gpu: If true, enables GPU acceleration for the browser session. Requires Start-Up or - Enterprise plan. + Enterprise plan and headless=false. headless: If true, launches the browser using a headless image (no VNC/GUI). Defaults to false. @@ -598,7 +598,7 @@ async def create( extensions: List of browser extensions to load into the session. Provide each by id or name. gpu: If true, enables GPU acceleration for the browser session. Requires Start-Up or - Enterprise plan. + Enterprise plan and headless=false. headless: If true, launches the browser using a headless image (no VNC/GUI). Defaults to false. diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index f59959e0..2827b1dc 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -23,7 +23,7 @@ class BrowserCreateParams(TypedDict, total=False): gpu: bool """If true, enables GPU acceleration for the browser session. - Requires Start-Up or Enterprise plan. + Requires Start-Up or Enterprise plan and headless=false. """ headless: bool diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index f1f8c781..d59a3d0d 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -45,7 +45,10 @@ class BrowserCreateResponse(BaseModel): """When the browser session was soft-deleted. Only present for deleted sessions.""" gpu: Optional[bool] = None - """Whether GPU acceleration is enabled for the browser session.""" + """ + Whether GPU acceleration is enabled for the browser session (only supported for + headful sessions). + """ kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index 8dbaf1dd..708caa97 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -45,7 +45,10 @@ class BrowserListResponse(BaseModel): """When the browser session was soft-deleted. Only present for deleted sessions.""" gpu: Optional[bool] = None - """Whether GPU acceleration is enabled for the browser session.""" + """ + Whether GPU acceleration is enabled for the browser session (only supported for + headful sessions). + """ kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py index 24e436ef..5ab52b58 100644 --- a/src/kernel/types/browser_pool_acquire_response.py +++ b/src/kernel/types/browser_pool_acquire_response.py @@ -45,7 +45,10 @@ class BrowserPoolAcquireResponse(BaseModel): """When the browser session was soft-deleted. Only present for deleted sessions.""" gpu: Optional[bool] = None - """Whether GPU acceleration is enabled for the browser session.""" + """ + Whether GPU acceleration is enabled for the browser session (only supported for + headful sessions). + """ kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 8b8353f5..221eab52 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -45,7 +45,10 @@ class BrowserRetrieveResponse(BaseModel): """When the browser session was soft-deleted. Only present for deleted sessions.""" gpu: Optional[bool] = None - """Whether GPU acceleration is enabled for the browser session.""" + """ + Whether GPU acceleration is enabled for the browser session (only supported for + headful sessions). + """ kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py index 5b90fa43..c8a85c3b 100644 --- a/src/kernel/types/browser_update_response.py +++ b/src/kernel/types/browser_update_response.py @@ -45,7 +45,10 @@ class BrowserUpdateResponse(BaseModel): """When the browser session was soft-deleted. Only present for deleted sessions.""" gpu: Optional[bool] = None - """Whether GPU acceleration is enabled for the browser session.""" + """ + Whether GPU acceleration is enabled for the browser session (only supported for + headful sessions). + """ kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py index 7e4225ec..a0fed9a2 100644 --- a/src/kernel/types/invocation_list_browsers_response.py +++ b/src/kernel/types/invocation_list_browsers_response.py @@ -45,7 +45,10 @@ class Browser(BaseModel): """When the browser session was soft-deleted. Only present for deleted sessions.""" gpu: Optional[bool] = None - """Whether GPU acceleration is enabled for the browser session.""" + """ + Whether GPU acceleration is enabled for the browser session (only supported for + headful sessions). + """ kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" From e3a48db5351b86dd63c91ff45f8424cd0b157aca Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 02:28:01 +0000 Subject: [PATCH 343/448] fix: sanitize endpoint path params --- src/kernel/_utils/__init__.py | 1 + src/kernel/_utils/_path.py | 127 +++++++++++++++++++ src/kernel/resources/auth/connections.py | 22 ++-- src/kernel/resources/browser_pools.py | 26 ++-- src/kernel/resources/browsers/browsers.py | 18 +-- src/kernel/resources/browsers/computer.py | 50 ++++---- src/kernel/resources/browsers/fs/fs.py | 50 ++++---- src/kernel/resources/browsers/fs/watch.py | 14 +- src/kernel/resources/browsers/logs.py | 6 +- src/kernel/resources/browsers/playwright.py | 6 +- src/kernel/resources/browsers/process.py | 30 ++--- src/kernel/resources/browsers/replays.py | 18 +-- src/kernel/resources/credential_providers.py | 22 ++-- src/kernel/resources/credentials.py | 18 +-- src/kernel/resources/deployments.py | 14 +- src/kernel/resources/extensions.py | 10 +- src/kernel/resources/invocations.py | 22 ++-- src/kernel/resources/profiles.py | 14 +- src/kernel/resources/proxies.py | 14 +- tests/test_utils/test_path.py | 89 +++++++++++++ 20 files changed, 394 insertions(+), 177 deletions(-) create mode 100644 src/kernel/_utils/_path.py create mode 100644 tests/test_utils/test_path.py diff --git a/src/kernel/_utils/__init__.py b/src/kernel/_utils/__init__.py index dc64e29a..10cb66d2 100644 --- a/src/kernel/_utils/__init__.py +++ b/src/kernel/_utils/__init__.py @@ -1,3 +1,4 @@ +from ._path import path_template as path_template from ._sync import asyncify as asyncify from ._proxy import LazyProxy as LazyProxy from ._utils import ( diff --git a/src/kernel/_utils/_path.py b/src/kernel/_utils/_path.py new file mode 100644 index 00000000..4d6e1e4c --- /dev/null +++ b/src/kernel/_utils/_path.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +import re +from typing import ( + Any, + Mapping, + Callable, +) +from urllib.parse import quote + +# Matches '.' or '..' where each dot is either literal or percent-encoded (%2e / %2E). +_DOT_SEGMENT_RE = re.compile(r"^(?:\.|%2[eE]){1,2}$") + +_PLACEHOLDER_RE = re.compile(r"\{(\w+)\}") + + +def _quote_path_segment_part(value: str) -> str: + """Percent-encode `value` for use in a URI path segment. + + Considers characters not in `pchar` set from RFC 3986 §3.3 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.3 + """ + # quote() already treats unreserved characters (letters, digits, and -._~) + # as safe, so we only need to add sub-delims, ':', and '@'. + # Notably, unlike the default `safe` for quote(), / is unsafe and must be quoted. + return quote(value, safe="!$&'()*+,;=:@") + + +def _quote_query_part(value: str) -> str: + """Percent-encode `value` for use in a URI query string. + + Considers &, = and characters not in `query` set from RFC 3986 §3.4 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.4 + """ + return quote(value, safe="!$'()*+,;:@/?") + + +def _quote_fragment_part(value: str) -> str: + """Percent-encode `value` for use in a URI fragment. + + Considers characters not in `fragment` set from RFC 3986 §3.5 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.5 + """ + return quote(value, safe="!$&'()*+,;=:@/?") + + +def _interpolate( + template: str, + values: Mapping[str, Any], + quoter: Callable[[str], str], +) -> str: + """Replace {name} placeholders in `template`, quoting each value with `quoter`. + + Placeholder names are looked up in `values`. + + Raises: + KeyError: If a placeholder is not found in `values`. + """ + # re.split with a capturing group returns alternating + # [text, name, text, name, ..., text] elements. + parts = _PLACEHOLDER_RE.split(template) + + for i in range(1, len(parts), 2): + name = parts[i] + if name not in values: + raise KeyError(f"a value for placeholder {{{name}}} was not provided") + val = values[name] + if val is None: + parts[i] = "null" + elif isinstance(val, bool): + parts[i] = "true" if val else "false" + else: + parts[i] = quoter(str(values[name])) + + return "".join(parts) + + +def path_template(template: str, /, **kwargs: Any) -> str: + """Interpolate {name} placeholders in `template` from keyword arguments. + + Args: + template: The template string containing {name} placeholders. + **kwargs: Keyword arguments to interpolate into the template. + + Returns: + The template with placeholders interpolated and percent-encoded. + + Safe characters for percent-encoding are dependent on the URI component. + Placeholders in path and fragment portions are percent-encoded where the `segment` + and `fragment` sets from RFC 3986 respectively are considered safe. + Placeholders in the query portion are percent-encoded where the `query` set from + RFC 3986 §3.3 is considered safe except for = and & characters. + + Raises: + KeyError: If a placeholder is not found in `kwargs`. + ValueError: If resulting path contains /./ or /../ segments (including percent-encoded dot-segments). + """ + # Split the template into path, query, and fragment portions. + fragment_template: str | None = None + query_template: str | None = None + + rest = template + if "#" in rest: + rest, fragment_template = rest.split("#", 1) + if "?" in rest: + rest, query_template = rest.split("?", 1) + path_template = rest + + # Interpolate each portion with the appropriate quoting rules. + path_result = _interpolate(path_template, kwargs, _quote_path_segment_part) + + # Reject dot-segments (. and ..) in the final assembled path. The check + # runs after interpolation so that adjacent placeholders or a mix of static + # text and placeholders that together form a dot-segment are caught. + # Also reject percent-encoded dot-segments to protect against incorrectly + # implemented normalization in servers/proxies. + for segment in path_result.split("/"): + if _DOT_SEGMENT_RE.match(segment): + raise ValueError(f"Constructed path {path_result!r} contains dot-segment {segment!r} which is not allowed") + + result = path_result + if query_template is not None: + result += "?" + _interpolate(query_template, kwargs, _quote_query_part) + if fragment_template is not None: + result += "#" + _interpolate(fragment_template, kwargs, _quote_fragment_part) + + return result diff --git a/src/kernel/resources/auth/connections.py b/src/kernel/resources/auth/connections.py index fed66797..0915365b 100644 --- a/src/kernel/resources/auth/connections.py +++ b/src/kernel/resources/auth/connections.py @@ -7,7 +7,7 @@ import httpx from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, SequenceNotStr, omit, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -179,7 +179,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/auth/connections/{id}", + path_template("/auth/connections/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -273,7 +273,7 @@ def delete( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( - f"/auth/connections/{id}", + path_template("/auth/connections/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -309,7 +309,7 @@ def follow( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} return self._get( - f"/auth/connections/{id}/events", + path_template("/auth/connections/{id}/events", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -353,7 +353,7 @@ def login( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/auth/connections/{id}/login", + path_template("/auth/connections/{id}/login", id=id), body=maybe_transform({"proxy": proxy}, connection_login_params.ConnectionLoginParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -398,7 +398,7 @@ def submit( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/auth/connections/{id}/submit", + path_template("/auth/connections/{id}/submit", id=id), body=maybe_transform( { "fields": fields, @@ -560,7 +560,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/auth/connections/{id}", + path_template("/auth/connections/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -654,7 +654,7 @@ async def delete( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( - f"/auth/connections/{id}", + path_template("/auth/connections/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -690,7 +690,7 @@ async def follow( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} return await self._get( - f"/auth/connections/{id}/events", + path_template("/auth/connections/{id}/events", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -734,7 +734,7 @@ async def login( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/auth/connections/{id}/login", + path_template("/auth/connections/{id}/login", id=id), body=await async_maybe_transform({"proxy": proxy}, connection_login_params.ConnectionLoginParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -779,7 +779,7 @@ async def submit( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/auth/connections/{id}/submit", + path_template("/auth/connections/{id}/submit", id=id), body=await async_maybe_transform( { "fields": fields, diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py index ea45cd13..c0ce6595 100644 --- a/src/kernel/resources/browser_pools.py +++ b/src/kernel/resources/browser_pools.py @@ -14,7 +14,7 @@ browser_pool_release_params, ) from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -180,7 +180,7 @@ def retrieve( if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return self._get( - f"/browser_pools/{id_or_name}", + path_template("/browser_pools/{id_or_name}", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -269,7 +269,7 @@ def update( if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return self._patch( - f"/browser_pools/{id_or_name}", + path_template("/browser_pools/{id_or_name}", id_or_name=id_or_name), body=maybe_transform( { "size": size, @@ -345,7 +345,7 @@ def delete( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( - f"/browser_pools/{id_or_name}", + path_template("/browser_pools/{id_or_name}", id_or_name=id_or_name), body=maybe_transform({"force": force}, browser_pool_delete_params.BrowserPoolDeleteParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -388,7 +388,7 @@ def acquire( if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return self._post( - f"/browser_pools/{id_or_name}/acquire", + path_template("/browser_pools/{id_or_name}/acquire", id_or_name=id_or_name), body=maybe_transform( {"acquire_timeout_seconds": acquire_timeout_seconds}, browser_pool_acquire_params.BrowserPoolAcquireParams, @@ -426,7 +426,7 @@ def flush( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._post( - f"/browser_pools/{id_or_name}/flush", + path_template("/browser_pools/{id_or_name}/flush", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -467,7 +467,7 @@ def release( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._post( - f"/browser_pools/{id_or_name}/release", + path_template("/browser_pools/{id_or_name}/release", id_or_name=id_or_name), body=maybe_transform( { "session_id": session_id, @@ -628,7 +628,7 @@ async def retrieve( if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return await self._get( - f"/browser_pools/{id_or_name}", + path_template("/browser_pools/{id_or_name}", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -717,7 +717,7 @@ async def update( if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return await self._patch( - f"/browser_pools/{id_or_name}", + path_template("/browser_pools/{id_or_name}", id_or_name=id_or_name), body=await async_maybe_transform( { "size": size, @@ -793,7 +793,7 @@ async def delete( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( - f"/browser_pools/{id_or_name}", + path_template("/browser_pools/{id_or_name}", id_or_name=id_or_name), body=await async_maybe_transform({"force": force}, browser_pool_delete_params.BrowserPoolDeleteParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -836,7 +836,7 @@ async def acquire( if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return await self._post( - f"/browser_pools/{id_or_name}/acquire", + path_template("/browser_pools/{id_or_name}/acquire", id_or_name=id_or_name), body=await async_maybe_transform( {"acquire_timeout_seconds": acquire_timeout_seconds}, browser_pool_acquire_params.BrowserPoolAcquireParams, @@ -874,7 +874,7 @@ async def flush( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._post( - f"/browser_pools/{id_or_name}/flush", + path_template("/browser_pools/{id_or_name}/flush", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -915,7 +915,7 @@ async def release( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._post( - f"/browser_pools/{id_or_name}/release", + path_template("/browser_pools/{id_or_name}/release", id_or_name=id_or_name), body=await async_maybe_transform( { "session_id": session_id, diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 1d1ce22a..c28a16c7 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -49,7 +49,7 @@ AsyncReplaysResourceWithStreamingResponse, ) from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given -from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform +from ..._utils import extract_files, path_template, maybe_transform, deepcopy_minimal, async_maybe_transform from .computer import ( ComputerResource, AsyncComputerResource, @@ -269,7 +269,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/browsers/{id}", + path_template("/browsers/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -319,7 +319,7 @@ def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._patch( - f"/browsers/{id}", + path_template("/browsers/{id}", id=id), body=maybe_transform( { "profile": profile, @@ -465,7 +465,7 @@ def delete_by_id( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( - f"/browsers/{id}", + path_template("/browsers/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -509,7 +509,7 @@ def load_extensions( # multipart/form-data; boundary=---abc-- extra_headers["Content-Type"] = "multipart/form-data" return self._post( - f"/browsers/{id}/extensions", + path_template("/browsers/{id}/extensions", id=id), body=maybe_transform(body, browser_load_extensions_params.BrowserLoadExtensionsParams), files=files, options=make_request_options( @@ -700,7 +700,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/browsers/{id}", + path_template("/browsers/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -750,7 +750,7 @@ async def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._patch( - f"/browsers/{id}", + path_template("/browsers/{id}", id=id), body=await async_maybe_transform( { "profile": profile, @@ -898,7 +898,7 @@ async def delete_by_id( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( - f"/browsers/{id}", + path_template("/browsers/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -942,7 +942,7 @@ async def load_extensions( # multipart/form-data; boundary=---abc-- extra_headers["Content-Type"] = "multipart/form-data" return await self._post( - f"/browsers/{id}/extensions", + path_template("/browsers/{id}/extensions", id=id), body=await async_maybe_transform(body, browser_load_extensions_params.BrowserLoadExtensionsParams), files=files, options=make_request_options( diff --git a/src/kernel/resources/browsers/computer.py b/src/kernel/resources/browsers/computer.py index 116a7037..54b638e5 100644 --- a/src/kernel/resources/browsers/computer.py +++ b/src/kernel/resources/browsers/computer.py @@ -8,7 +8,7 @@ import httpx from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, SequenceNotStr, omit, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -97,7 +97,7 @@ def batch( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._post( - f"/browsers/{id}/computer/batch", + path_template("/browsers/{id}/computer/batch", id=id), body=maybe_transform({"actions": actions}, computer_batch_params.ComputerBatchParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -133,7 +133,7 @@ def capture_screenshot( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "image/png", **(extra_headers or {})} return self._post( - f"/browsers/{id}/computer/screenshot", + path_template("/browsers/{id}/computer/screenshot", id=id), body=maybe_transform( {"region": region}, computer_capture_screenshot_params.ComputerCaptureScreenshotParams ), @@ -188,7 +188,7 @@ def click_mouse( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._post( - f"/browsers/{id}/computer/click_mouse", + path_template("/browsers/{id}/computer/click_mouse", id=id), body=maybe_transform( { "x": x, @@ -261,7 +261,7 @@ def drag_mouse( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._post( - f"/browsers/{id}/computer/drag_mouse", + path_template("/browsers/{id}/computer/drag_mouse", id=id), body=maybe_transform( { "path": path, @@ -307,7 +307,7 @@ def get_mouse_position( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/browsers/{id}/computer/get_mouse_position", + path_template("/browsers/{id}/computer/get_mouse_position", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -357,7 +357,7 @@ def move_mouse( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._post( - f"/browsers/{id}/computer/move_mouse", + path_template("/browsers/{id}/computer/move_mouse", id=id), body=maybe_transform( { "x": x, @@ -414,7 +414,7 @@ def press_key( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._post( - f"/browsers/{id}/computer/press_key", + path_template("/browsers/{id}/computer/press_key", id=id), body=maybe_transform( { "keys": keys, @@ -455,7 +455,7 @@ def read_clipboard( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/browsers/{id}/computer/clipboard/read", + path_template("/browsers/{id}/computer/clipboard/read", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -506,7 +506,7 @@ def scroll( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._post( - f"/browsers/{id}/computer/scroll", + path_template("/browsers/{id}/computer/scroll", id=id), body=maybe_transform( { "x": x, @@ -552,7 +552,7 @@ def set_cursor_visibility( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/browsers/{id}/computer/cursor", + path_template("/browsers/{id}/computer/cursor", id=id), body=maybe_transform( {"hidden": hidden}, computer_set_cursor_visibility_params.ComputerSetCursorVisibilityParams ), @@ -595,7 +595,7 @@ def type_text( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._post( - f"/browsers/{id}/computer/type", + path_template("/browsers/{id}/computer/type", id=id), body=maybe_transform( { "text": text, @@ -639,7 +639,7 @@ def write_clipboard( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._post( - f"/browsers/{id}/computer/clipboard/write", + path_template("/browsers/{id}/computer/clipboard/write", id=id), body=maybe_transform({"text": text}, computer_write_clipboard_params.ComputerWriteClipboardParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -700,7 +700,7 @@ async def batch( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._post( - f"/browsers/{id}/computer/batch", + path_template("/browsers/{id}/computer/batch", id=id), body=await async_maybe_transform({"actions": actions}, computer_batch_params.ComputerBatchParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -736,7 +736,7 @@ async def capture_screenshot( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "image/png", **(extra_headers or {})} return await self._post( - f"/browsers/{id}/computer/screenshot", + path_template("/browsers/{id}/computer/screenshot", id=id), body=await async_maybe_transform( {"region": region}, computer_capture_screenshot_params.ComputerCaptureScreenshotParams ), @@ -791,7 +791,7 @@ async def click_mouse( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._post( - f"/browsers/{id}/computer/click_mouse", + path_template("/browsers/{id}/computer/click_mouse", id=id), body=await async_maybe_transform( { "x": x, @@ -864,7 +864,7 @@ async def drag_mouse( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._post( - f"/browsers/{id}/computer/drag_mouse", + path_template("/browsers/{id}/computer/drag_mouse", id=id), body=await async_maybe_transform( { "path": path, @@ -910,7 +910,7 @@ async def get_mouse_position( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/browsers/{id}/computer/get_mouse_position", + path_template("/browsers/{id}/computer/get_mouse_position", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -960,7 +960,7 @@ async def move_mouse( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._post( - f"/browsers/{id}/computer/move_mouse", + path_template("/browsers/{id}/computer/move_mouse", id=id), body=await async_maybe_transform( { "x": x, @@ -1017,7 +1017,7 @@ async def press_key( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._post( - f"/browsers/{id}/computer/press_key", + path_template("/browsers/{id}/computer/press_key", id=id), body=await async_maybe_transform( { "keys": keys, @@ -1058,7 +1058,7 @@ async def read_clipboard( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/browsers/{id}/computer/clipboard/read", + path_template("/browsers/{id}/computer/clipboard/read", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -1109,7 +1109,7 @@ async def scroll( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._post( - f"/browsers/{id}/computer/scroll", + path_template("/browsers/{id}/computer/scroll", id=id), body=await async_maybe_transform( { "x": x, @@ -1155,7 +1155,7 @@ async def set_cursor_visibility( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/browsers/{id}/computer/cursor", + path_template("/browsers/{id}/computer/cursor", id=id), body=await async_maybe_transform( {"hidden": hidden}, computer_set_cursor_visibility_params.ComputerSetCursorVisibilityParams ), @@ -1198,7 +1198,7 @@ async def type_text( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._post( - f"/browsers/{id}/computer/type", + path_template("/browsers/{id}/computer/type", id=id), body=await async_maybe_transform( { "text": text, @@ -1242,7 +1242,7 @@ async def write_clipboard( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._post( - f"/browsers/{id}/computer/clipboard/write", + path_template("/browsers/{id}/computer/clipboard/write", id=id), body=await async_maybe_transform( {"text": text}, computer_write_clipboard_params.ComputerWriteClipboardParams ), diff --git a/src/kernel/resources/browsers/fs/fs.py b/src/kernel/resources/browsers/fs/fs.py index 1bd16afb..f26119fb 100644 --- a/src/kernel/resources/browsers/fs/fs.py +++ b/src/kernel/resources/browsers/fs/fs.py @@ -30,7 +30,7 @@ omit, not_given, ) -from ...._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform +from ...._utils import extract_files, path_template, maybe_transform, deepcopy_minimal, async_maybe_transform from ...._compat import cached_property from ...._resource import SyncAPIResource, AsyncAPIResource from ...._response import ( @@ -128,7 +128,7 @@ def create_directory( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._put( - f"/browsers/{id}/fs/create_directory", + path_template("/browsers/{id}/fs/create_directory", id=id), body=maybe_transform( { "path": path, @@ -172,7 +172,7 @@ def delete_directory( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._put( - f"/browsers/{id}/fs/delete_directory", + path_template("/browsers/{id}/fs/delete_directory", id=id), body=maybe_transform({"path": path}, f_delete_directory_params.FDeleteDirectoryParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -210,7 +210,7 @@ def delete_file( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._put( - f"/browsers/{id}/fs/delete_file", + path_template("/browsers/{id}/fs/delete_file", id=id), body=maybe_transform({"path": path}, f_delete_file_params.FDeleteFileParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -248,7 +248,7 @@ def download_dir_zip( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "application/zip", **(extra_headers or {})} return self._get( - f"/browsers/{id}/fs/download_dir_zip", + path_template("/browsers/{id}/fs/download_dir_zip", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -288,7 +288,7 @@ def file_info( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/browsers/{id}/fs/file_info", + path_template("/browsers/{id}/fs/file_info", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -328,7 +328,7 @@ def list_files( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/browsers/{id}/fs/list_files", + path_template("/browsers/{id}/fs/list_files", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -372,7 +372,7 @@ def move( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._put( - f"/browsers/{id}/fs/move", + path_template("/browsers/{id}/fs/move", id=id), body=maybe_transform( { "dest_path": dest_path, @@ -416,7 +416,7 @@ def read_file( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} return self._get( - f"/browsers/{id}/fs/read_file", + path_template("/browsers/{id}/fs/read_file", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -466,7 +466,7 @@ def set_file_permissions( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._put( - f"/browsers/{id}/fs/set_file_permissions", + path_template("/browsers/{id}/fs/set_file_permissions", id=id), body=maybe_transform( { "mode": mode, @@ -516,7 +516,7 @@ def upload( # multipart/form-data; boundary=---abc-- extra_headers["Content-Type"] = "multipart/form-data" return self._post( - f"/browsers/{id}/fs/upload", + path_template("/browsers/{id}/fs/upload", id=id), body=maybe_transform(body, f_upload_params.FUploadParams), files=extracted_files, options=make_request_options( @@ -567,7 +567,7 @@ def upload_zip( # multipart/form-data; boundary=---abc-- extra_headers["Content-Type"] = "multipart/form-data" return self._post( - f"/browsers/{id}/fs/upload_zip", + path_template("/browsers/{id}/fs/upload_zip", id=id), body=maybe_transform(body, f_upload_zip_params.FUploadZipParams), files=files, options=make_request_options( @@ -611,7 +611,7 @@ def write_file( extra_headers = {"Accept": "*/*", **(extra_headers or {})} extra_headers["Content-Type"] = "application/octet-stream" return self._put( - f"/browsers/{id}/fs/write_file", + path_template("/browsers/{id}/fs/write_file", id=id), content=read_file_content(contents) if isinstance(contents, os.PathLike) else contents, options=make_request_options( extra_headers=extra_headers, @@ -690,7 +690,7 @@ async def create_directory( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._put( - f"/browsers/{id}/fs/create_directory", + path_template("/browsers/{id}/fs/create_directory", id=id), body=await async_maybe_transform( { "path": path, @@ -734,7 +734,7 @@ async def delete_directory( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._put( - f"/browsers/{id}/fs/delete_directory", + path_template("/browsers/{id}/fs/delete_directory", id=id), body=await async_maybe_transform({"path": path}, f_delete_directory_params.FDeleteDirectoryParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -772,7 +772,7 @@ async def delete_file( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._put( - f"/browsers/{id}/fs/delete_file", + path_template("/browsers/{id}/fs/delete_file", id=id), body=await async_maybe_transform({"path": path}, f_delete_file_params.FDeleteFileParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -810,7 +810,7 @@ async def download_dir_zip( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "application/zip", **(extra_headers or {})} return await self._get( - f"/browsers/{id}/fs/download_dir_zip", + path_template("/browsers/{id}/fs/download_dir_zip", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -850,7 +850,7 @@ async def file_info( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/browsers/{id}/fs/file_info", + path_template("/browsers/{id}/fs/file_info", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -890,7 +890,7 @@ async def list_files( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/browsers/{id}/fs/list_files", + path_template("/browsers/{id}/fs/list_files", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -934,7 +934,7 @@ async def move( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._put( - f"/browsers/{id}/fs/move", + path_template("/browsers/{id}/fs/move", id=id), body=await async_maybe_transform( { "dest_path": dest_path, @@ -978,7 +978,7 @@ async def read_file( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} return await self._get( - f"/browsers/{id}/fs/read_file", + path_template("/browsers/{id}/fs/read_file", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -1028,7 +1028,7 @@ async def set_file_permissions( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._put( - f"/browsers/{id}/fs/set_file_permissions", + path_template("/browsers/{id}/fs/set_file_permissions", id=id), body=await async_maybe_transform( { "mode": mode, @@ -1078,7 +1078,7 @@ async def upload( # multipart/form-data; boundary=---abc-- extra_headers["Content-Type"] = "multipart/form-data" return await self._post( - f"/browsers/{id}/fs/upload", + path_template("/browsers/{id}/fs/upload", id=id), body=await async_maybe_transform(body, f_upload_params.FUploadParams), files=extracted_files, options=make_request_options( @@ -1129,7 +1129,7 @@ async def upload_zip( # multipart/form-data; boundary=---abc-- extra_headers["Content-Type"] = "multipart/form-data" return await self._post( - f"/browsers/{id}/fs/upload_zip", + path_template("/browsers/{id}/fs/upload_zip", id=id), body=await async_maybe_transform(body, f_upload_zip_params.FUploadZipParams), files=files, options=make_request_options( @@ -1173,7 +1173,7 @@ async def write_file( extra_headers = {"Accept": "*/*", **(extra_headers or {})} extra_headers["Content-Type"] = "application/octet-stream" return await self._put( - f"/browsers/{id}/fs/write_file", + path_template("/browsers/{id}/fs/write_file", id=id), content=await async_read_file_content(contents) if isinstance(contents, os.PathLike) else contents, options=make_request_options( extra_headers=extra_headers, diff --git a/src/kernel/resources/browsers/fs/watch.py b/src/kernel/resources/browsers/fs/watch.py index ca438673..bc046053 100644 --- a/src/kernel/resources/browsers/fs/watch.py +++ b/src/kernel/resources/browsers/fs/watch.py @@ -5,7 +5,7 @@ import httpx from ...._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given -from ...._utils import maybe_transform, async_maybe_transform +from ...._utils import path_template, maybe_transform, async_maybe_transform from ...._compat import cached_property from ...._resource import SyncAPIResource, AsyncAPIResource from ...._response import ( @@ -75,7 +75,7 @@ def events( raise ValueError(f"Expected a non-empty value for `watch_id` but received {watch_id!r}") extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} return self._get( - f"/browsers/{id}/fs/watch/{watch_id}/events", + path_template("/browsers/{id}/fs/watch/{watch_id}/events", id=id, watch_id=watch_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -116,7 +116,7 @@ def start( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/browsers/{id}/fs/watch", + path_template("/browsers/{id}/fs/watch", id=id), body=maybe_transform( { "path": path, @@ -160,7 +160,7 @@ def stop( raise ValueError(f"Expected a non-empty value for `watch_id` but received {watch_id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( - f"/browsers/{id}/fs/watch/{watch_id}", + path_template("/browsers/{id}/fs/watch/{watch_id}", id=id, watch_id=watch_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -220,7 +220,7 @@ async def events( raise ValueError(f"Expected a non-empty value for `watch_id` but received {watch_id!r}") extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} return await self._get( - f"/browsers/{id}/fs/watch/{watch_id}/events", + path_template("/browsers/{id}/fs/watch/{watch_id}/events", id=id, watch_id=watch_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -261,7 +261,7 @@ async def start( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/browsers/{id}/fs/watch", + path_template("/browsers/{id}/fs/watch", id=id), body=await async_maybe_transform( { "path": path, @@ -305,7 +305,7 @@ async def stop( raise ValueError(f"Expected a non-empty value for `watch_id` but received {watch_id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( - f"/browsers/{id}/fs/watch/{watch_id}", + path_template("/browsers/{id}/fs/watch/{watch_id}", id=id, watch_id=watch_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/kernel/resources/browsers/logs.py b/src/kernel/resources/browsers/logs.py index 01328551..35ee66dc 100644 --- a/src/kernel/resources/browsers/logs.py +++ b/src/kernel/resources/browsers/logs.py @@ -7,7 +7,7 @@ import httpx from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -81,7 +81,7 @@ def stream( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} return self._get( - f"/browsers/{id}/logs/stream", + path_template("/browsers/{id}/logs/stream", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -160,7 +160,7 @@ async def stream( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} return await self._get( - f"/browsers/{id}/logs/stream", + path_template("/browsers/{id}/logs/stream", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, diff --git a/src/kernel/resources/browsers/playwright.py b/src/kernel/resources/browsers/playwright.py index 6979a9de..8d261ed5 100644 --- a/src/kernel/resources/browsers/playwright.py +++ b/src/kernel/resources/browsers/playwright.py @@ -5,7 +5,7 @@ import httpx from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -82,7 +82,7 @@ def execute( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/browsers/{id}/playwright/execute", + path_template("/browsers/{id}/playwright/execute", id=id), body=maybe_transform( { "code": code, @@ -158,7 +158,7 @@ async def execute( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/browsers/{id}/playwright/execute", + path_template("/browsers/{id}/playwright/execute", id=id), body=await async_maybe_transform( { "code": code, diff --git a/src/kernel/resources/browsers/process.py b/src/kernel/resources/browsers/process.py index 86752a5e..83827d38 100644 --- a/src/kernel/resources/browsers/process.py +++ b/src/kernel/resources/browsers/process.py @@ -8,7 +8,7 @@ import httpx from ..._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -106,7 +106,7 @@ def exec( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/browsers/{id}/process/exec", + path_template("/browsers/{id}/process/exec", id=id), body=maybe_transform( { "command": command, @@ -157,7 +157,7 @@ def kill( if not process_id: raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}") return self._post( - f"/browsers/{id}/process/{process_id}/kill", + path_template("/browsers/{id}/process/{process_id}/kill", id=id, process_id=process_id), body=maybe_transform({"signal": signal}, process_kill_params.ProcessKillParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -200,7 +200,7 @@ def resize( if not process_id: raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}") return self._post( - f"/browsers/{id}/process/{process_id}/resize", + path_template("/browsers/{id}/process/{process_id}/resize", id=id, process_id=process_id), body=maybe_transform( { "cols": cols, @@ -270,7 +270,7 @@ def spawn( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/browsers/{id}/process/spawn", + path_template("/browsers/{id}/process/spawn", id=id), body=maybe_transform( { "command": command, @@ -321,7 +321,7 @@ def status( if not process_id: raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}") return self._get( - f"/browsers/{id}/process/{process_id}/status", + path_template("/browsers/{id}/process/{process_id}/status", id=id, process_id=process_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -360,7 +360,7 @@ def stdin( if not process_id: raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}") return self._post( - f"/browsers/{id}/process/{process_id}/stdin", + path_template("/browsers/{id}/process/{process_id}/stdin", id=id, process_id=process_id), body=maybe_transform({"data_b64": data_b64}, process_stdin_params.ProcessStdinParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -398,7 +398,7 @@ def stdout_stream( raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}") extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} return self._get( - f"/browsers/{id}/process/{process_id}/stdout/stream", + path_template("/browsers/{id}/process/{process_id}/stdout/stream", id=id, process_id=process_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -477,7 +477,7 @@ async def exec( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/browsers/{id}/process/exec", + path_template("/browsers/{id}/process/exec", id=id), body=await async_maybe_transform( { "command": command, @@ -528,7 +528,7 @@ async def kill( if not process_id: raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}") return await self._post( - f"/browsers/{id}/process/{process_id}/kill", + path_template("/browsers/{id}/process/{process_id}/kill", id=id, process_id=process_id), body=await async_maybe_transform({"signal": signal}, process_kill_params.ProcessKillParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -571,7 +571,7 @@ async def resize( if not process_id: raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}") return await self._post( - f"/browsers/{id}/process/{process_id}/resize", + path_template("/browsers/{id}/process/{process_id}/resize", id=id, process_id=process_id), body=await async_maybe_transform( { "cols": cols, @@ -641,7 +641,7 @@ async def spawn( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/browsers/{id}/process/spawn", + path_template("/browsers/{id}/process/spawn", id=id), body=await async_maybe_transform( { "command": command, @@ -692,7 +692,7 @@ async def status( if not process_id: raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}") return await self._get( - f"/browsers/{id}/process/{process_id}/status", + path_template("/browsers/{id}/process/{process_id}/status", id=id, process_id=process_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -731,7 +731,7 @@ async def stdin( if not process_id: raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}") return await self._post( - f"/browsers/{id}/process/{process_id}/stdin", + path_template("/browsers/{id}/process/{process_id}/stdin", id=id, process_id=process_id), body=await async_maybe_transform({"data_b64": data_b64}, process_stdin_params.ProcessStdinParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -769,7 +769,7 @@ async def stdout_stream( raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}") extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} return await self._get( - f"/browsers/{id}/process/{process_id}/stdout/stream", + path_template("/browsers/{id}/process/{process_id}/stdout/stream", id=id, process_id=process_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/kernel/resources/browsers/replays.py b/src/kernel/resources/browsers/replays.py index 743a6668..2b20953a 100644 --- a/src/kernel/resources/browsers/replays.py +++ b/src/kernel/resources/browsers/replays.py @@ -5,7 +5,7 @@ import httpx from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -78,7 +78,7 @@ def list( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/browsers/{id}/replays", + path_template("/browsers/{id}/replays", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -115,7 +115,7 @@ def download( raise ValueError(f"Expected a non-empty value for `replay_id` but received {replay_id!r}") extra_headers = {"Accept": "video/mp4", **(extra_headers or {})} return self._get( - f"/browsers/{id}/replays/{replay_id}", + path_template("/browsers/{id}/replays/{replay_id}", id=id, replay_id=replay_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -154,7 +154,7 @@ def start( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/browsers/{id}/replays", + path_template("/browsers/{id}/replays", id=id), body=maybe_transform( { "framerate": framerate, @@ -198,7 +198,7 @@ def stop( raise ValueError(f"Expected a non-empty value for `replay_id` but received {replay_id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._post( - f"/browsers/{id}/replays/{replay_id}/stop", + path_template("/browsers/{id}/replays/{replay_id}/stop", id=id, replay_id=replay_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -254,7 +254,7 @@ async def list( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/browsers/{id}/replays", + path_template("/browsers/{id}/replays", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -291,7 +291,7 @@ async def download( raise ValueError(f"Expected a non-empty value for `replay_id` but received {replay_id!r}") extra_headers = {"Accept": "video/mp4", **(extra_headers or {})} return await self._get( - f"/browsers/{id}/replays/{replay_id}", + path_template("/browsers/{id}/replays/{replay_id}", id=id, replay_id=replay_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -330,7 +330,7 @@ async def start( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/browsers/{id}/replays", + path_template("/browsers/{id}/replays", id=id), body=await async_maybe_transform( { "framerate": framerate, @@ -374,7 +374,7 @@ async def stop( raise ValueError(f"Expected a non-empty value for `replay_id` but received {replay_id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._post( - f"/browsers/{id}/replays/{replay_id}/stop", + path_template("/browsers/{id}/replays/{replay_id}/stop", id=id, replay_id=replay_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/kernel/resources/credential_providers.py b/src/kernel/resources/credential_providers.py index c7ad4b00..2dede2c4 100644 --- a/src/kernel/resources/credential_providers.py +++ b/src/kernel/resources/credential_providers.py @@ -8,7 +8,7 @@ from ..types import credential_provider_create_params, credential_provider_update_params from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -126,7 +126,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/org/credential_providers/{id}", + path_template("/org/credential_providers/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -174,7 +174,7 @@ def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._patch( - f"/org/credential_providers/{id}", + path_template("/org/credential_providers/{id}", id=id), body=maybe_transform( { "token": token, @@ -237,7 +237,7 @@ def delete( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( - f"/org/credential_providers/{id}", + path_template("/org/credential_providers/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -271,7 +271,7 @@ def list_items( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/org/credential_providers/{id}/items", + path_template("/org/credential_providers/{id}/items", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -304,7 +304,7 @@ def test( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/org/credential_providers/{id}/test", + path_template("/org/credential_providers/{id}/test", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -412,7 +412,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/org/credential_providers/{id}", + path_template("/org/credential_providers/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -460,7 +460,7 @@ async def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._patch( - f"/org/credential_providers/{id}", + path_template("/org/credential_providers/{id}", id=id), body=await async_maybe_transform( { "token": token, @@ -523,7 +523,7 @@ async def delete( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( - f"/org/credential_providers/{id}", + path_template("/org/credential_providers/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -557,7 +557,7 @@ async def list_items( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/org/credential_providers/{id}/items", + path_template("/org/credential_providers/{id}/items", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -590,7 +590,7 @@ async def test( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/org/credential_providers/{id}/test", + path_template("/org/credential_providers/{id}/test", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/kernel/resources/credentials.py b/src/kernel/resources/credentials.py index 000c7675..093fcc5d 100644 --- a/src/kernel/resources/credentials.py +++ b/src/kernel/resources/credentials.py @@ -8,7 +8,7 @@ from ..types import credential_list_params, credential_create_params, credential_update_params from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -133,7 +133,7 @@ def retrieve( if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return self._get( - f"/credentials/{id_or_name}", + path_template("/credentials/{id_or_name}", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -183,7 +183,7 @@ def update( if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return self._patch( - f"/credentials/{id_or_name}", + path_template("/credentials/{id_or_name}", id_or_name=id_or_name), body=maybe_transform( { "name": name, @@ -279,7 +279,7 @@ def delete( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( - f"/credentials/{id_or_name}", + path_template("/credentials/{id_or_name}", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -314,7 +314,7 @@ def totp_code( if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return self._get( - f"/credentials/{id_or_name}/totp-code", + path_template("/credentials/{id_or_name}/totp-code", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -430,7 +430,7 @@ async def retrieve( if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return await self._get( - f"/credentials/{id_or_name}", + path_template("/credentials/{id_or_name}", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -480,7 +480,7 @@ async def update( if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return await self._patch( - f"/credentials/{id_or_name}", + path_template("/credentials/{id_or_name}", id_or_name=id_or_name), body=await async_maybe_transform( { "name": name, @@ -576,7 +576,7 @@ async def delete( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( - f"/credentials/{id_or_name}", + path_template("/credentials/{id_or_name}", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -611,7 +611,7 @@ async def totp_code( if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return await self._get( - f"/credentials/{id_or_name}/totp-code", + path_template("/credentials/{id_or_name}/totp-code", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/kernel/resources/deployments.py b/src/kernel/resources/deployments.py index b6a72d2d..6b2c7601 100644 --- a/src/kernel/resources/deployments.py +++ b/src/kernel/resources/deployments.py @@ -9,7 +9,7 @@ from ..types import deployment_list_params, deployment_create_params, deployment_follow_params from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, FileTypes, omit, not_given -from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform +from .._utils import extract_files, path_template, maybe_transform, deepcopy_minimal, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -147,7 +147,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/deployments/{id}", + path_template("/deployments/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -239,7 +239,7 @@ def delete( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( - f"/deployments/{id}", + path_template("/deployments/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -278,7 +278,7 @@ def follow( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} return self._get( - f"/deployments/{id}/events", + path_template("/deployments/{id}/events", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -412,7 +412,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/deployments/{id}", + path_template("/deployments/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -504,7 +504,7 @@ async def delete( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( - f"/deployments/{id}", + path_template("/deployments/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -543,7 +543,7 @@ async def follow( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} return await self._get( - f"/deployments/{id}/events", + path_template("/deployments/{id}/events", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, diff --git a/src/kernel/resources/extensions.py b/src/kernel/resources/extensions.py index ffdef29e..c429b3c5 100644 --- a/src/kernel/resources/extensions.py +++ b/src/kernel/resources/extensions.py @@ -9,7 +9,7 @@ from ..types import extension_upload_params, extension_download_from_chrome_store_params from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, FileTypes, omit, not_given -from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform +from .._utils import extract_files, path_template, maybe_transform, deepcopy_minimal, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -101,7 +101,7 @@ def delete( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( - f"/extensions/{id_or_name}", + path_template("/extensions/{id_or_name}", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -135,7 +135,7 @@ def download( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} return self._get( - f"/extensions/{id_or_name}", + path_template("/extensions/{id_or_name}", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -310,7 +310,7 @@ async def delete( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( - f"/extensions/{id_or_name}", + path_template("/extensions/{id_or_name}", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -344,7 +344,7 @@ async def download( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} return await self._get( - f"/extensions/{id_or_name}", + path_template("/extensions/{id_or_name}", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/kernel/resources/invocations.py b/src/kernel/resources/invocations.py index 25be409f..6d367e9e 100644 --- a/src/kernel/resources/invocations.py +++ b/src/kernel/resources/invocations.py @@ -9,7 +9,7 @@ from ..types import invocation_list_params, invocation_create_params, invocation_follow_params, invocation_update_params from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -140,7 +140,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/invocations/{id}", + path_template("/invocations/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -181,7 +181,7 @@ def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._patch( - f"/invocations/{id}", + path_template("/invocations/{id}", id=id), body=maybe_transform( { "status": status, @@ -296,7 +296,7 @@ def delete_browsers( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( - f"/invocations/{id}/browsers", + path_template("/invocations/{id}/browsers", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -335,7 +335,7 @@ def follow( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} return self._get( - f"/invocations/{id}/events", + path_template("/invocations/{id}/events", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -376,7 +376,7 @@ def list_browsers( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/invocations/{id}/browsers", + path_template("/invocations/{id}/browsers", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -493,7 +493,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/invocations/{id}", + path_template("/invocations/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -534,7 +534,7 @@ async def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._patch( - f"/invocations/{id}", + path_template("/invocations/{id}", id=id), body=await async_maybe_transform( { "status": status, @@ -649,7 +649,7 @@ async def delete_browsers( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( - f"/invocations/{id}/browsers", + path_template("/invocations/{id}/browsers", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -688,7 +688,7 @@ async def follow( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} return await self._get( - f"/invocations/{id}/events", + path_template("/invocations/{id}/events", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -729,7 +729,7 @@ async def list_browsers( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/invocations/{id}/browsers", + path_template("/invocations/{id}/browsers", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/kernel/resources/profiles.py b/src/kernel/resources/profiles.py index f75569f2..ec3d3fcc 100644 --- a/src/kernel/resources/profiles.py +++ b/src/kernel/resources/profiles.py @@ -6,7 +6,7 @@ from ..types import profile_list_params, profile_create_params from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -113,7 +113,7 @@ def retrieve( if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return self._get( - f"/profiles/{id_or_name}", + path_template("/profiles/{id_or_name}", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -198,7 +198,7 @@ def delete( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( - f"/profiles/{id_or_name}", + path_template("/profiles/{id_or_name}", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -234,7 +234,7 @@ def download( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} return self._get( - f"/profiles/{id_or_name}/download", + path_template("/profiles/{id_or_name}/download", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -325,7 +325,7 @@ async def retrieve( if not id_or_name: raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return await self._get( - f"/profiles/{id_or_name}", + path_template("/profiles/{id_or_name}", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -410,7 +410,7 @@ async def delete( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( - f"/profiles/{id_or_name}", + path_template("/profiles/{id_or_name}", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -446,7 +446,7 @@ async def download( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} return await self._get( - f"/profiles/{id_or_name}/download", + path_template("/profiles/{id_or_name}/download", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/kernel/resources/proxies.py b/src/kernel/resources/proxies.py index 0c2508c0..f259d6ed 100644 --- a/src/kernel/resources/proxies.py +++ b/src/kernel/resources/proxies.py @@ -8,7 +8,7 @@ from ..types import proxy_create_params from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, SequenceNotStr, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -130,7 +130,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/proxies/{id}", + path_template("/proxies/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -184,7 +184,7 @@ def delete( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( - f"/proxies/{id}", + path_template("/proxies/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -217,7 +217,7 @@ def check( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/proxies/{id}/check", + path_template("/proxies/{id}/check", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -329,7 +329,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/proxies/{id}", + path_template("/proxies/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -383,7 +383,7 @@ async def delete( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( - f"/proxies/{id}", + path_template("/proxies/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -416,7 +416,7 @@ async def check( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/proxies/{id}/check", + path_template("/proxies/{id}/check", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/tests/test_utils/test_path.py b/tests/test_utils/test_path.py new file mode 100644 index 00000000..80b86802 --- /dev/null +++ b/tests/test_utils/test_path.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +from typing import Any + +import pytest + +from kernel._utils._path import path_template + + +@pytest.mark.parametrize( + "template, kwargs, expected", + [ + ("/v1/{id}", dict(id="abc"), "/v1/abc"), + ("/v1/{a}/{b}", dict(a="x", b="y"), "/v1/x/y"), + ("/v1/{a}{b}/path/{c}?val={d}#{e}", dict(a="x", b="y", c="z", d="u", e="v"), "/v1/xy/path/z?val=u#v"), + ("/{w}/{w}", dict(w="echo"), "/echo/echo"), + ("/v1/static", {}, "/v1/static"), + ("", {}, ""), + ("/v1/?q={n}&count=10", dict(n=42), "/v1/?q=42&count=10"), + ("/v1/{v}", dict(v=None), "/v1/null"), + ("/v1/{v}", dict(v=True), "/v1/true"), + ("/v1/{v}", dict(v=False), "/v1/false"), + ("/v1/{v}", dict(v=".hidden"), "/v1/.hidden"), # dot prefix ok + ("/v1/{v}", dict(v="file.txt"), "/v1/file.txt"), # dot in middle ok + ("/v1/{v}", dict(v="..."), "/v1/..."), # triple dot ok + ("/v1/{a}{b}", dict(a=".", b="txt"), "/v1/.txt"), # dot var combining with adjacent to be ok + ("/items?q={v}#{f}", dict(v=".", f=".."), "/items?q=.#.."), # dots in query/fragment are fine + ( + "/v1/{a}?query={b}", + dict(a="../../other/endpoint", b="a&bad=true"), + "/v1/..%2F..%2Fother%2Fendpoint?query=a%26bad%3Dtrue", + ), + ("/v1/{val}", dict(val="a/b/c"), "/v1/a%2Fb%2Fc"), + ("/v1/{val}", dict(val="a/b/c?query=value"), "/v1/a%2Fb%2Fc%3Fquery=value"), + ("/v1/{val}", dict(val="a/b/c?query=value&bad=true"), "/v1/a%2Fb%2Fc%3Fquery=value&bad=true"), + ("/v1/{val}", dict(val="%20"), "/v1/%2520"), # escapes escape sequences in input + # Query: slash and ? are safe, # is not + ("/items?q={v}", dict(v="a/b"), "/items?q=a/b"), + ("/items?q={v}", dict(v="a?b"), "/items?q=a?b"), + ("/items?q={v}", dict(v="a#b"), "/items?q=a%23b"), + ("/items?q={v}", dict(v="a b"), "/items?q=a%20b"), + # Fragment: slash and ? are safe + ("/docs#{v}", dict(v="a/b"), "/docs#a/b"), + ("/docs#{v}", dict(v="a?b"), "/docs#a?b"), + # Path: slash, ? and # are all encoded + ("/v1/{v}", dict(v="a/b"), "/v1/a%2Fb"), + ("/v1/{v}", dict(v="a?b"), "/v1/a%3Fb"), + ("/v1/{v}", dict(v="a#b"), "/v1/a%23b"), + # same var encoded differently by component + ( + "/v1/{v}?q={v}#{v}", + dict(v="a/b?c#d"), + "/v1/a%2Fb%3Fc%23d?q=a/b?c%23d#a/b?c%23d", + ), + ("/v1/{val}", dict(val="x?admin=true"), "/v1/x%3Fadmin=true"), # query injection + ("/v1/{val}", dict(val="x#admin"), "/v1/x%23admin"), # fragment injection + ], +) +def test_interpolation(template: str, kwargs: dict[str, Any], expected: str) -> None: + assert path_template(template, **kwargs) == expected + + +def test_missing_kwarg_raises_key_error() -> None: + with pytest.raises(KeyError, match="org_id"): + path_template("/v1/{org_id}") + + +@pytest.mark.parametrize( + "template, kwargs", + [ + ("{a}/path", dict(a=".")), + ("{a}/path", dict(a="..")), + ("/v1/{a}", dict(a=".")), + ("/v1/{a}", dict(a="..")), + ("/v1/{a}/path", dict(a=".")), + ("/v1/{a}/path", dict(a="..")), + ("/v1/{a}{b}", dict(a=".", b=".")), # adjacent vars → ".." + ("/v1/{a}.", dict(a=".")), # var + static → ".." + ("/v1/{a}{b}", dict(a="", b=".")), # empty + dot → "." + ("/v1/%2e/{x}", dict(x="ok")), # encoded dot in static text + ("/v1/%2e./{x}", dict(x="ok")), # mixed encoded ".." in static + ("/v1/.%2E/{x}", dict(x="ok")), # mixed encoded ".." in static + ("/v1/{v}?q=1", dict(v="..")), + ("/v1/{v}#frag", dict(v="..")), + ], +) +def test_dot_segment_rejected(template: str, kwargs: dict[str, Any]) -> None: + with pytest.raises(ValueError, match="dot-segment"): + path_template(template, **kwargs) From af88ec24d8440e9164cd5243b0add6e32ab47b6f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:33:10 +0000 Subject: [PATCH 344/448] feat: Enhance managed authentication with CUA support and new features --- .stats.yml | 8 +- api.md | 2 + src/kernel/resources/auth/connections.py | 181 +++++++++++++++++- src/kernel/types/auth/__init__.py | 1 + .../types/auth/connection_follow_response.py | 23 +++ .../types/auth/connection_submit_params.py | 16 +- .../types/auth/connection_update_params.py | 72 +++++++ src/kernel/types/auth/managed_auth.py | 24 ++- tests/api_resources/auth/test_connections.py | 132 +++++++++++++ 9 files changed, 448 insertions(+), 11 deletions(-) create mode 100644 src/kernel/types/auth/connection_update_params.py diff --git a/.stats.yml b/.stats.yml index ad84265a..be60802f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 103 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-17e50cf93d8052ff655c160fc0f156621d9029b041526d4e2e3317b13f80822f.yml -openapi_spec_hash: f7dadc8d93e77983936eb18a8080ce15 -config_hash: cff4d43372b6fa66b64e2d4150f6aa76 +configured_endpoints: 104 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-bb2ac8e0d3a1c08e8afcbcbad7cb733d0f84bd22a8d233c1ec3100a01ee078ae.yml +openapi_spec_hash: a83f7d1c422c85d6dc6158af7afe1d09 +config_hash: 16e4457a0bb26e98a335a1c2a572290a diff --git a/api.md b/api.md index cce73021..696c481b 100644 --- a/api.md +++ b/api.md @@ -245,6 +245,7 @@ from kernel.types.auth import ( LoginResponse, ManagedAuth, ManagedAuthCreateRequest, + ManagedAuthUpdateRequest, SubmitFieldsRequest, SubmitFieldsResponse, ConnectionFollowResponse, @@ -255,6 +256,7 @@ Methods: - client.auth.connections.create(\*\*params) -> ManagedAuth - client.auth.connections.retrieve(id) -> ManagedAuth +- client.auth.connections.update(id, \*\*params) -> ManagedAuth - client.auth.connections.list(\*\*params) -> SyncOffsetPagination[ManagedAuth] - client.auth.connections.delete(id) -> None - client.auth.connections.follow(id) -> ConnectionFollowResponse diff --git a/src/kernel/resources/auth/connections.py b/src/kernel/resources/auth/connections.py index 0915365b..c610da4c 100644 --- a/src/kernel/resources/auth/connections.py +++ b/src/kernel/resources/auth/connections.py @@ -23,6 +23,7 @@ connection_login_params, connection_create_params, connection_submit_params, + connection_update_params, ) from ..._base_client import AsyncPaginator, make_request_options from ...types.auth.managed_auth import ManagedAuth @@ -186,6 +187,76 @@ def retrieve( cast_to=ManagedAuth, ) + def update( + self, + id: str, + *, + allowed_domains: SequenceNotStr[str] | Omit = omit, + credential: connection_update_params.Credential | Omit = omit, + health_check_interval: int | Omit = omit, + login_url: str | Omit = omit, + proxy: connection_update_params.Proxy | Omit = omit, + save_credentials: bool | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ManagedAuth: + """Update an auth connection's configuration. + + Only the fields provided will be + updated. + + Args: + allowed_domains: Additional domains valid for this auth flow (replaces existing list) + + credential: + Reference to credentials for the auth connection. Use one of: + + - { name } for Kernel credentials + - { provider, path } for external provider item + - { provider, auto: true } for external provider domain lookup + + health_check_interval: Interval in seconds between automatic health checks + + login_url: Login page URL. Set to empty string to clear. + + proxy: Proxy selection. Provide either id or name. The proxy must belong to the + caller's org. + + save_credentials: Whether to save credentials after every successful login + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._patch( + path_template("/auth/connections/{id}", id=id), + body=maybe_transform( + { + "allowed_domains": allowed_domains, + "credential": credential, + "health_check_interval": health_check_interval, + "login_url": login_url, + "proxy": proxy, + "save_credentials": save_credentials, + }, + connection_update_params.ConnectionUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ManagedAuth, + ) + def list( self, *, @@ -367,7 +438,9 @@ def submit( *, fields: Dict[str, str] | Omit = omit, mfa_option_id: str | Omit = omit, + sign_in_option_id: str | Omit = omit, sso_button_selector: str | Omit = omit, + sso_provider: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -383,9 +456,15 @@ def submit( Args: fields: Map of field name to value - mfa_option_id: Optional MFA option ID if user selected an MFA method + mfa_option_id: The MFA method type to select (when mfa_options were returned) + + sign_in_option_id: The sign-in option ID to select (when sign_in_options were returned) - sso_button_selector: Optional XPath selector if user chose to click an SSO button instead + sso_button_selector: XPath selector for the SSO button to click (ODA). Use sso_provider instead for + CUA. + + sso_provider: SSO provider to click, matching the provider field from pending_sso_buttons + (e.g., "google", "github"). Cannot be used with sso_button_selector. extra_headers: Send extra headers @@ -403,7 +482,9 @@ def submit( { "fields": fields, "mfa_option_id": mfa_option_id, + "sign_in_option_id": sign_in_option_id, "sso_button_selector": sso_button_selector, + "sso_provider": sso_provider, }, connection_submit_params.ConnectionSubmitParams, ), @@ -567,6 +648,76 @@ async def retrieve( cast_to=ManagedAuth, ) + async def update( + self, + id: str, + *, + allowed_domains: SequenceNotStr[str] | Omit = omit, + credential: connection_update_params.Credential | Omit = omit, + health_check_interval: int | Omit = omit, + login_url: str | Omit = omit, + proxy: connection_update_params.Proxy | Omit = omit, + save_credentials: bool | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ManagedAuth: + """Update an auth connection's configuration. + + Only the fields provided will be + updated. + + Args: + allowed_domains: Additional domains valid for this auth flow (replaces existing list) + + credential: + Reference to credentials for the auth connection. Use one of: + + - { name } for Kernel credentials + - { provider, path } for external provider item + - { provider, auto: true } for external provider domain lookup + + health_check_interval: Interval in seconds between automatic health checks + + login_url: Login page URL. Set to empty string to clear. + + proxy: Proxy selection. Provide either id or name. The proxy must belong to the + caller's org. + + save_credentials: Whether to save credentials after every successful login + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._patch( + path_template("/auth/connections/{id}", id=id), + body=await async_maybe_transform( + { + "allowed_domains": allowed_domains, + "credential": credential, + "health_check_interval": health_check_interval, + "login_url": login_url, + "proxy": proxy, + "save_credentials": save_credentials, + }, + connection_update_params.ConnectionUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ManagedAuth, + ) + def list( self, *, @@ -748,7 +899,9 @@ async def submit( *, fields: Dict[str, str] | Omit = omit, mfa_option_id: str | Omit = omit, + sign_in_option_id: str | Omit = omit, sso_button_selector: str | Omit = omit, + sso_provider: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -764,9 +917,15 @@ async def submit( Args: fields: Map of field name to value - mfa_option_id: Optional MFA option ID if user selected an MFA method + mfa_option_id: The MFA method type to select (when mfa_options were returned) + + sign_in_option_id: The sign-in option ID to select (when sign_in_options were returned) - sso_button_selector: Optional XPath selector if user chose to click an SSO button instead + sso_button_selector: XPath selector for the SSO button to click (ODA). Use sso_provider instead for + CUA. + + sso_provider: SSO provider to click, matching the provider field from pending_sso_buttons + (e.g., "google", "github"). Cannot be used with sso_button_selector. extra_headers: Send extra headers @@ -784,7 +943,9 @@ async def submit( { "fields": fields, "mfa_option_id": mfa_option_id, + "sign_in_option_id": sign_in_option_id, "sso_button_selector": sso_button_selector, + "sso_provider": sso_provider, }, connection_submit_params.ConnectionSubmitParams, ), @@ -805,6 +966,9 @@ def __init__(self, connections: ConnectionsResource) -> None: self.retrieve = to_raw_response_wrapper( connections.retrieve, ) + self.update = to_raw_response_wrapper( + connections.update, + ) self.list = to_raw_response_wrapper( connections.list, ) @@ -832,6 +996,9 @@ def __init__(self, connections: AsyncConnectionsResource) -> None: self.retrieve = async_to_raw_response_wrapper( connections.retrieve, ) + self.update = async_to_raw_response_wrapper( + connections.update, + ) self.list = async_to_raw_response_wrapper( connections.list, ) @@ -859,6 +1026,9 @@ def __init__(self, connections: ConnectionsResource) -> None: self.retrieve = to_streamed_response_wrapper( connections.retrieve, ) + self.update = to_streamed_response_wrapper( + connections.update, + ) self.list = to_streamed_response_wrapper( connections.list, ) @@ -886,6 +1056,9 @@ def __init__(self, connections: AsyncConnectionsResource) -> None: self.retrieve = async_to_streamed_response_wrapper( connections.retrieve, ) + self.update = async_to_streamed_response_wrapper( + connections.update, + ) self.list = async_to_streamed_response_wrapper( connections.list, ) diff --git a/src/kernel/types/auth/__init__.py b/src/kernel/types/auth/__init__.py index 51e505bf..db897944 100644 --- a/src/kernel/types/auth/__init__.py +++ b/src/kernel/types/auth/__init__.py @@ -9,4 +9,5 @@ from .connection_login_params import ConnectionLoginParams as ConnectionLoginParams from .connection_create_params import ConnectionCreateParams as ConnectionCreateParams from .connection_submit_params import ConnectionSubmitParams as ConnectionSubmitParams +from .connection_update_params import ConnectionUpdateParams as ConnectionUpdateParams from .connection_follow_response import ConnectionFollowResponse as ConnectionFollowResponse diff --git a/src/kernel/types/auth/connection_follow_response.py b/src/kernel/types/auth/connection_follow_response.py index 06ffaeab..4eeba5c5 100644 --- a/src/kernel/types/auth/connection_follow_response.py +++ b/src/kernel/types/auth/connection_follow_response.py @@ -15,6 +15,7 @@ "ManagedAuthStateEventDiscoveredField", "ManagedAuthStateEventMfaOption", "ManagedAuthStateEventPendingSSOButton", + "ManagedAuthStateEventSignInOption", ] @@ -77,6 +78,22 @@ class ManagedAuthStateEventPendingSSOButton(BaseModel): """XPath selector for the button""" +class ManagedAuthStateEventSignInOption(BaseModel): + """A non-MFA choice presented during the auth flow (e.g. + + account selection, org picker) + """ + + id: str + """Unique identifier for this option (used to submit selection back)""" + + label: str + """Display text for the option""" + + description: Optional[str] = None + """Additional context such as email address or org name""" + + class ManagedAuthStateEvent(BaseModel): """An event representing the current state of a managed auth flow.""" @@ -128,6 +145,12 @@ class ManagedAuthStateEvent(BaseModel): post_login_url: Optional[str] = None """URL where the browser landed after successful login.""" + sign_in_options: Optional[List[ManagedAuthStateEventSignInOption]] = None + """ + Non-MFA choices presented during the auth flow, such as account selection or org + pickers (present when flow_step=AWAITING_INPUT). + """ + website_error: Optional[str] = None """Visible error message from the website (e.g., 'Incorrect password'). diff --git a/src/kernel/types/auth/connection_submit_params.py b/src/kernel/types/auth/connection_submit_params.py index 0e2306ac..f785b856 100644 --- a/src/kernel/types/auth/connection_submit_params.py +++ b/src/kernel/types/auth/connection_submit_params.py @@ -13,7 +13,19 @@ class ConnectionSubmitParams(TypedDict, total=False): """Map of field name to value""" mfa_option_id: str - """Optional MFA option ID if user selected an MFA method""" + """The MFA method type to select (when mfa_options were returned)""" + + sign_in_option_id: str + """The sign-in option ID to select (when sign_in_options were returned)""" sso_button_selector: str - """Optional XPath selector if user chose to click an SSO button instead""" + """XPath selector for the SSO button to click (ODA). + + Use sso_provider instead for CUA. + """ + + sso_provider: str + """ + SSO provider to click, matching the provider field from pending_sso_buttons + (e.g., "google", "github"). Cannot be used with sso_button_selector. + """ diff --git a/src/kernel/types/auth/connection_update_params.py b/src/kernel/types/auth/connection_update_params.py new file mode 100644 index 00000000..77e738b7 --- /dev/null +++ b/src/kernel/types/auth/connection_update_params.py @@ -0,0 +1,72 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +from ..._types import SequenceNotStr + +__all__ = ["ConnectionUpdateParams", "Credential", "Proxy"] + + +class ConnectionUpdateParams(TypedDict, total=False): + allowed_domains: SequenceNotStr[str] + """Additional domains valid for this auth flow (replaces existing list)""" + + credential: Credential + """Reference to credentials for the auth connection. Use one of: + + - { name } for Kernel credentials + - { provider, path } for external provider item + - { provider, auto: true } for external provider domain lookup + """ + + health_check_interval: int + """Interval in seconds between automatic health checks""" + + login_url: str + """Login page URL. Set to empty string to clear.""" + + proxy: Proxy + """Proxy selection. + + Provide either id or name. The proxy must belong to the caller's org. + """ + + save_credentials: bool + """Whether to save credentials after every successful login""" + + +class Credential(TypedDict, total=False): + """Reference to credentials for the auth connection. + + Use one of: + - { name } for Kernel credentials + - { provider, path } for external provider item + - { provider, auto: true } for external provider domain lookup + """ + + auto: bool + """If true, lookup by domain from the specified provider""" + + name: str + """Kernel credential name""" + + path: str + """Provider-specific path (e.g., "VaultName/ItemName" for 1Password)""" + + provider: str + """External provider name (e.g., "my-1p")""" + + +class Proxy(TypedDict, total=False): + """Proxy selection. + + Provide either id or name. The proxy must belong to the caller's org. + """ + + id: str + """Proxy ID""" + + name: str + """Proxy name""" diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py index de607c9e..9f90bc5d 100644 --- a/src/kernel/types/auth/managed_auth.py +++ b/src/kernel/types/auth/managed_auth.py @@ -6,7 +6,7 @@ from ..._models import BaseModel -__all__ = ["ManagedAuth", "Credential", "DiscoveredField", "MfaOption", "PendingSSOButton"] +__all__ = ["ManagedAuth", "Credential", "DiscoveredField", "MfaOption", "PendingSSOButton", "SignInOption"] class Credential(BaseModel): @@ -90,6 +90,22 @@ class PendingSSOButton(BaseModel): """XPath selector for the button""" +class SignInOption(BaseModel): + """A non-MFA choice presented during the auth flow (e.g. + + account selection, org picker) + """ + + id: str + """Unique identifier for this option (used to submit selection back)""" + + label: str + """Display text for the option""" + + description: Optional[str] = None + """Additional context such as email address or org name""" + + class ManagedAuth(BaseModel): """Managed authentication that keeps a profile logged into a specific domain. @@ -214,6 +230,12 @@ class ManagedAuth(BaseModel): proxy_id: Optional[str] = None """ID of the proxy associated with this connection, if any.""" + sign_in_options: Optional[List[SignInOption]] = None + """ + Non-MFA choices presented during the auth flow, such as account selection or org + pickers (present when flow_step=awaiting_input). + """ + sso_provider: Optional[str] = None """SSO provider being used (e.g., google, github, microsoft)""" diff --git a/tests/api_resources/auth/test_connections.py b/tests/api_resources/auth/test_connections.py index e71a1736..f5edd89d 100644 --- a/tests/api_resources/auth/test_connections.py +++ b/tests/api_resources/auth/test_connections.py @@ -124,6 +124,70 @@ def test_path_params_retrieve(self, client: Kernel) -> None: "", ) + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update(self, client: Kernel) -> None: + connection = client.auth.connections.update( + id="id", + ) + assert_matches_type(ManagedAuth, connection, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update_with_all_params(self, client: Kernel) -> None: + connection = client.auth.connections.update( + id="id", + allowed_domains=["login.netflix.com", "auth.netflix.com"], + credential={ + "auto": True, + "name": "my-netflix-creds", + "path": "Personal/Netflix", + "provider": "my-1p", + }, + health_check_interval=3600, + login_url="https://netflix.com/login", + proxy={ + "id": "id", + "name": "name", + }, + save_credentials=True, + ) + assert_matches_type(ManagedAuth, connection, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_update(self, client: Kernel) -> None: + response = client.auth.connections.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + connection = response.parse() + assert_matches_type(ManagedAuth, connection, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_update(self, client: Kernel) -> None: + with client.auth.connections.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + connection = response.parse() + assert_matches_type(ManagedAuth, connection, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_update(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.auth.connections.with_raw_response.update( + id="", + ) + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_list(self, client: Kernel) -> None: @@ -318,7 +382,9 @@ def test_method_submit_with_all_params(self, client: Kernel) -> None: "password": "secret", }, mfa_option_id="sms", + sign_in_option_id="work-account", sso_button_selector="xpath=//button[contains(text(), 'Continue with Google')]", + sso_provider="google", ) assert_matches_type(SubmitFieldsResponse, connection, path=["response"]) @@ -464,6 +530,70 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: "", ) + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update(self, async_client: AsyncKernel) -> None: + connection = await async_client.auth.connections.update( + id="id", + ) + assert_matches_type(ManagedAuth, connection, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> None: + connection = await async_client.auth.connections.update( + id="id", + allowed_domains=["login.netflix.com", "auth.netflix.com"], + credential={ + "auto": True, + "name": "my-netflix-creds", + "path": "Personal/Netflix", + "provider": "my-1p", + }, + health_check_interval=3600, + login_url="https://netflix.com/login", + proxy={ + "id": "id", + "name": "name", + }, + save_credentials=True, + ) + assert_matches_type(ManagedAuth, connection, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_update(self, async_client: AsyncKernel) -> None: + response = await async_client.auth.connections.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + connection = await response.parse() + assert_matches_type(ManagedAuth, connection, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_update(self, async_client: AsyncKernel) -> None: + async with async_client.auth.connections.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + connection = await response.parse() + assert_matches_type(ManagedAuth, connection, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_update(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.auth.connections.with_raw_response.update( + id="", + ) + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_list(self, async_client: AsyncKernel) -> None: @@ -658,7 +788,9 @@ async def test_method_submit_with_all_params(self, async_client: AsyncKernel) -> "password": "secret", }, mfa_option_id="sms", + sign_in_option_id="work-account", sso_button_selector="xpath=//button[contains(text(), 'Continue with Google')]", + sso_provider="google", ) assert_matches_type(SubmitFieldsResponse, connection, path=["response"]) From 66f702505bd21c9ed42f406f907d0f2411bc60a4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:36:06 +0000 Subject: [PATCH 345/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index fe87cd91..cc51f6f8 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.43.0" + ".": "0.44.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 62a14748..62ca8334 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.43.0" +version = "0.44.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 068f52c7..2c4ce41e 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.43.0" # x-release-please-version +__version__ = "0.44.0" # x-release-please-version From a0838c007ca389f91a2ef09a240b47e2ab6662d3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 02:56:29 +0000 Subject: [PATCH 346/448] chore(internal): update gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 95ceb189..3824f4c4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .prism.log +.stdy.log _dev __pycache__ From 43c500da20d2e5e6ffc4efbac4809ec027db52fd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 02:31:16 +0000 Subject: [PATCH 347/448] chore(ci): skip lint on metadata-only changes Note that we still want to run tests, as these depend on the metadata. --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c7bc99ae..fafebca7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: timeout-minutes: 10 name: lint runs-on: ${{ github.repository == 'stainless-sdks/kernel-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - uses: actions/checkout@v6 @@ -38,7 +38,7 @@ jobs: run: ./scripts/lint build: - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') timeout-minutes: 10 name: build permissions: From d74fe5fcb642b533f0d9da3c3614f769ce648f1a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 14:39:04 +0000 Subject: [PATCH 348/448] feat: [kernel-1008] browser pools add custom policy --- .stats.yml | 4 +-- src/kernel/resources/browser_pools.py | 30 ++++++++++++++++++- src/kernel/types/browser_pool.py | 10 ++++++- .../types/browser_pool_create_params.py | 10 ++++++- .../types/browser_pool_update_params.py | 10 ++++++- tests/api_resources/test_browser_pools.py | 4 +++ 6 files changed, 62 insertions(+), 6 deletions(-) diff --git a/.stats.yml b/.stats.yml index be60802f..8a5c9d0b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 104 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-bb2ac8e0d3a1c08e8afcbcbad7cb733d0f84bd22a8d233c1ec3100a01ee078ae.yml -openapi_spec_hash: a83f7d1c422c85d6dc6158af7afe1d09 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-aeb5ea5c2632fe7fd905d509bc6cbb06999d17c458ec44ffd713935ba5b848f9.yml +openapi_spec_hash: fef45a8569f1d3de04c86e95b1112665 config_hash: 16e4457a0bb26e98a335a1c2a572290a diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py index c0ce6595..045737af 100644 --- a/src/kernel/resources/browser_pools.py +++ b/src/kernel/resources/browser_pools.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Iterable +from typing import Dict, Iterable import httpx @@ -60,6 +60,7 @@ def create( self, *, size: int, + chrome_policy: Dict[str, object] | Omit = omit, extensions: Iterable[BrowserExtension] | Omit = omit, fill_rate_per_minute: int | Omit = omit, headless: bool | Omit = omit, @@ -85,6 +86,11 @@ def create( your organization's pooled sessions limit (the sum of all pool sizes cannot exceed your limit). + chrome_policy: Custom Chrome enterprise policy overrides applied to all browsers in this pool. + Keys are Chrome enterprise policy names; values must match their expected types. + Blocked: kernel-managed policies (extensions, proxy, CDP/automation). See + https://chromeenterprise.google/policies/ + extensions: List of browser extensions to load into the session. Provide each by id or name. fill_rate_per_minute: Percentage of the pool to fill per minute. Defaults to 10%. @@ -135,6 +141,7 @@ def create( body=maybe_transform( { "size": size, + "chrome_policy": chrome_policy, "extensions": extensions, "fill_rate_per_minute": fill_rate_per_minute, "headless": headless, @@ -192,6 +199,7 @@ def update( id_or_name: str, *, size: int, + chrome_policy: Dict[str, object] | Omit = omit, discard_all_idle: bool | Omit = omit, extensions: Iterable[BrowserExtension] | Omit = omit, fill_rate_per_minute: int | Omit = omit, @@ -218,6 +226,11 @@ def update( your organization's pooled sessions limit (the sum of all pool sizes cannot exceed your limit). + chrome_policy: Custom Chrome enterprise policy overrides applied to all browsers in this pool. + Keys are Chrome enterprise policy names; values must match their expected types. + Blocked: kernel-managed policies (extensions, proxy, CDP/automation). See + https://chromeenterprise.google/policies/ + discard_all_idle: Whether to discard all idle browsers and rebuild the pool immediately. Defaults to false. @@ -273,6 +286,7 @@ def update( body=maybe_transform( { "size": size, + "chrome_policy": chrome_policy, "discard_all_idle": discard_all_idle, "extensions": extensions, "fill_rate_per_minute": fill_rate_per_minute, @@ -508,6 +522,7 @@ async def create( self, *, size: int, + chrome_policy: Dict[str, object] | Omit = omit, extensions: Iterable[BrowserExtension] | Omit = omit, fill_rate_per_minute: int | Omit = omit, headless: bool | Omit = omit, @@ -533,6 +548,11 @@ async def create( your organization's pooled sessions limit (the sum of all pool sizes cannot exceed your limit). + chrome_policy: Custom Chrome enterprise policy overrides applied to all browsers in this pool. + Keys are Chrome enterprise policy names; values must match their expected types. + Blocked: kernel-managed policies (extensions, proxy, CDP/automation). See + https://chromeenterprise.google/policies/ + extensions: List of browser extensions to load into the session. Provide each by id or name. fill_rate_per_minute: Percentage of the pool to fill per minute. Defaults to 10%. @@ -583,6 +603,7 @@ async def create( body=await async_maybe_transform( { "size": size, + "chrome_policy": chrome_policy, "extensions": extensions, "fill_rate_per_minute": fill_rate_per_minute, "headless": headless, @@ -640,6 +661,7 @@ async def update( id_or_name: str, *, size: int, + chrome_policy: Dict[str, object] | Omit = omit, discard_all_idle: bool | Omit = omit, extensions: Iterable[BrowserExtension] | Omit = omit, fill_rate_per_minute: int | Omit = omit, @@ -666,6 +688,11 @@ async def update( your organization's pooled sessions limit (the sum of all pool sizes cannot exceed your limit). + chrome_policy: Custom Chrome enterprise policy overrides applied to all browsers in this pool. + Keys are Chrome enterprise policy names; values must match their expected types. + Blocked: kernel-managed policies (extensions, proxy, CDP/automation). See + https://chromeenterprise.google/policies/ + discard_all_idle: Whether to discard all idle browsers and rebuild the pool immediately. Defaults to false. @@ -721,6 +748,7 @@ async def update( body=await async_maybe_transform( { "size": size, + "chrome_policy": chrome_policy, "discard_all_idle": discard_all_idle, "extensions": extensions, "fill_rate_per_minute": fill_rate_per_minute, diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py index c6286acc..8ca0dc43 100644 --- a/src/kernel/types/browser_pool.py +++ b/src/kernel/types/browser_pool.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List, Optional +from typing import Dict, List, Optional from datetime import datetime from .._models import BaseModel @@ -21,6 +21,14 @@ class BrowserPoolConfig(BaseModel): sum of all pool sizes cannot exceed your limit). """ + chrome_policy: Optional[Dict[str, object]] = None + """Custom Chrome enterprise policy overrides applied to all browsers in this pool. + + Keys are Chrome enterprise policy names; values must match their expected types. + Blocked: kernel-managed policies (extensions, proxy, CDP/automation). See + https://chromeenterprise.google/policies/ + """ + extensions: Optional[List[BrowserExtension]] = None """List of browser extensions to load into the session. diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py index 63ef712b..ecfb8881 100644 --- a/src/kernel/types/browser_pool_create_params.py +++ b/src/kernel/types/browser_pool_create_params.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Iterable +from typing import Dict, Iterable from typing_extensions import Required, TypedDict from .shared_params.browser_profile import BrowserProfile @@ -20,6 +20,14 @@ class BrowserPoolCreateParams(TypedDict, total=False): sum of all pool sizes cannot exceed your limit). """ + chrome_policy: Dict[str, object] + """Custom Chrome enterprise policy overrides applied to all browsers in this pool. + + Keys are Chrome enterprise policy names; values must match their expected types. + Blocked: kernel-managed policies (extensions, proxy, CDP/automation). See + https://chromeenterprise.google/policies/ + """ + extensions: Iterable[BrowserExtension] """List of browser extensions to load into the session. diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py index d1f003b5..e34664a4 100644 --- a/src/kernel/types/browser_pool_update_params.py +++ b/src/kernel/types/browser_pool_update_params.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Iterable +from typing import Dict, Iterable from typing_extensions import Required, TypedDict from .shared_params.browser_profile import BrowserProfile @@ -20,6 +20,14 @@ class BrowserPoolUpdateParams(TypedDict, total=False): sum of all pool sizes cannot exceed your limit). """ + chrome_policy: Dict[str, object] + """Custom Chrome enterprise policy overrides applied to all browsers in this pool. + + Keys are Chrome enterprise policy names; values must match their expected types. + Blocked: kernel-managed policies (extensions, proxy, CDP/automation). See + https://chromeenterprise.google/policies/ + """ + discard_all_idle: bool """Whether to discard all idle browsers and rebuild the pool immediately. diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py index c7e1a477..70c7ad85 100644 --- a/tests/api_resources/test_browser_pools.py +++ b/tests/api_resources/test_browser_pools.py @@ -34,6 +34,7 @@ def test_method_create(self, client: Kernel) -> None: def test_method_create_with_all_params(self, client: Kernel) -> None: browser_pool = client.browser_pools.create( size=10, + chrome_policy={"foo": "bar"}, extensions=[ { "id": "id", @@ -143,6 +144,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None: browser_pool = client.browser_pools.update( id_or_name="id_or_name", size=10, + chrome_policy={"foo": "bar"}, discard_all_idle=False, extensions=[ { @@ -454,6 +456,7 @@ async def test_method_create(self, async_client: AsyncKernel) -> None: async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: browser_pool = await async_client.browser_pools.create( size=10, + chrome_policy={"foo": "bar"}, extensions=[ { "id": "id", @@ -563,6 +566,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> browser_pool = await async_client.browser_pools.update( id_or_name="id_or_name", size=10, + chrome_policy={"foo": "bar"}, discard_all_idle=False, extensions=[ { From db9e585eacb344c798a0c28e44d84ffe941ef6b3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 03:49:01 +0000 Subject: [PATCH 349/448] feat(internal): implement indices array format for query and form serialization --- src/kernel/_qs.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/kernel/_qs.py b/src/kernel/_qs.py index ada6fd3f..de8c99bc 100644 --- a/src/kernel/_qs.py +++ b/src/kernel/_qs.py @@ -101,7 +101,10 @@ def _stringify_item( items.extend(self._stringify_item(key, item, opts)) return items elif array_format == "indices": - raise NotImplementedError("The array indices format is not supported yet") + items = [] + for i, item in enumerate(value): + items.extend(self._stringify_item(f"{key}[{i}]", item, opts)) + return items elif array_format == "brackets": items = [] key = key + "[]" From 5ba0ee8fd198a7df6aa647131fcbd2149448b83e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 18:01:13 +0000 Subject: [PATCH 350/448] feat: Add disable_default_proxy for stealth browsers --- .stats.yml | 4 ++-- src/kernel/resources/browsers/browsers.py | 10 ++++++++++ src/kernel/types/browser_update_params.py | 6 ++++++ tests/api_resources/test_browsers.py | 2 ++ 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 8a5c9d0b..8ed1b33b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 104 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-aeb5ea5c2632fe7fd905d509bc6cbb06999d17c458ec44ffd713935ba5b848f9.yml -openapi_spec_hash: fef45a8569f1d3de04c86e95b1112665 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-20310988401243aa5c4a2e2ac6cba5dd90873fb7b83497a2d50c691352c0dd7b.yml +openapi_spec_hash: e19e650b4b2c8c8fde1f739c4aab6b33 config_hash: 16e4457a0bb26e98a335a1c2a572290a diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index c28a16c7..078f31fb 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -286,6 +286,7 @@ def update( self, id: str, *, + disable_default_proxy: bool | Omit = omit, profile: BrowserProfile | Omit = omit, proxy_id: Optional[str] | Omit = omit, viewport: browser_update_params.Viewport | Omit = omit, @@ -300,6 +301,9 @@ def update( Update a browser session. Args: + disable_default_proxy: If true, stealth browsers connect directly instead of using the default stealth + proxy. + profile: Profile to load into the browser session. Only allowed if the session does not already have a profile loaded. @@ -322,6 +326,7 @@ def update( path_template("/browsers/{id}", id=id), body=maybe_transform( { + "disable_default_proxy": disable_default_proxy, "profile": profile, "proxy_id": proxy_id, "viewport": viewport, @@ -717,6 +722,7 @@ async def update( self, id: str, *, + disable_default_proxy: bool | Omit = omit, profile: BrowserProfile | Omit = omit, proxy_id: Optional[str] | Omit = omit, viewport: browser_update_params.Viewport | Omit = omit, @@ -731,6 +737,9 @@ async def update( Update a browser session. Args: + disable_default_proxy: If true, stealth browsers connect directly instead of using the default stealth + proxy. + profile: Profile to load into the browser session. Only allowed if the session does not already have a profile loaded. @@ -753,6 +762,7 @@ async def update( path_template("/browsers/{id}", id=id), body=await async_maybe_transform( { + "disable_default_proxy": disable_default_proxy, "profile": profile, "proxy_id": proxy_id, "viewport": viewport, diff --git a/src/kernel/types/browser_update_params.py b/src/kernel/types/browser_update_params.py index 5c211948..e0f7588e 100644 --- a/src/kernel/types/browser_update_params.py +++ b/src/kernel/types/browser_update_params.py @@ -12,6 +12,12 @@ class BrowserUpdateParams(TypedDict, total=False): + disable_default_proxy: bool + """ + If true, stealth browsers connect directly instead of using the default stealth + proxy. + """ + profile: BrowserProfile """Profile to load into the browser session. diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index 39c5bef4..72c7b023 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -148,6 +148,7 @@ def test_method_update(self, client: Kernel) -> None: def test_method_update_with_all_params(self, client: Kernel) -> None: browser = client.browsers.update( id="htzv5orfit78e1m2biiifpbv", + disable_default_proxy=True, profile={ "id": "id", "name": "name", @@ -512,6 +513,7 @@ async def test_method_update(self, async_client: AsyncKernel) -> None: async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> None: browser = await async_client.browsers.update( id="htzv5orfit78e1m2biiifpbv", + disable_default_proxy=True, profile={ "id": "id", "name": "name", From 2561eb46c9dea11c341f0b471b9c68d6e5d92c36 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 18:23:32 +0000 Subject: [PATCH 351/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index cc51f6f8..fc0d7ff8 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.44.0" + ".": "0.45.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 62ca8334..b8b563e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.44.0" +version = "0.45.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 2c4ce41e..4483ae96 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.44.0" # x-release-please-version +__version__ = "0.45.0" # x-release-please-version From b0143b1c4950510f6dac5850ae285451d7ad38b0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 23:18:04 +0000 Subject: [PATCH 352/448] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 8ed1b33b..1bda9d34 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 104 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-20310988401243aa5c4a2e2ac6cba5dd90873fb7b83497a2d50c691352c0dd7b.yml -openapi_spec_hash: e19e650b4b2c8c8fde1f739c4aab6b33 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-403eadeddcd92ecf5c0ada739fb59d73d829a9b463788a81ac3ed97d0cd3a64b.yml +openapi_spec_hash: 8fdd3a5bd5e035f0adeb72329c215ad7 config_hash: 16e4457a0bb26e98a335a1c2a572290a From 6320800bed9917600d02e37da88142df06acc4e6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:48:26 +0000 Subject: [PATCH 353/448] feat: Add optional url parameter to proxy check endpoint --- .stats.yml | 4 +-- api.md | 2 +- src/kernel/resources/proxies.py | 48 +++++++++++++++++++++++--- src/kernel/types/__init__.py | 1 + src/kernel/types/proxy_check_params.py | 23 ++++++++++++ tests/api_resources/test_proxies.py | 34 +++++++++++++----- 6 files changed, 96 insertions(+), 16 deletions(-) create mode 100644 src/kernel/types/proxy_check_params.py diff --git a/.stats.yml b/.stats.yml index 1bda9d34..4bc313ec 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 104 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-403eadeddcd92ecf5c0ada739fb59d73d829a9b463788a81ac3ed97d0cd3a64b.yml -openapi_spec_hash: 8fdd3a5bd5e035f0adeb72329c215ad7 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-f7024f4171c7c4ec558de1c27f338b1089ffddd0d2dbfdb9bb9f9c2abe8f47bf.yml +openapi_spec_hash: ced43682b49e73a2862f99b49abb4fcd config_hash: 16e4457a0bb26e98a335a1c2a572290a diff --git a/api.md b/api.md index 696c481b..5e43066c 100644 --- a/api.md +++ b/api.md @@ -282,7 +282,7 @@ Methods: - client.proxies.retrieve(id) -> ProxyRetrieveResponse - client.proxies.list() -> ProxyListResponse - client.proxies.delete(id) -> None -- client.proxies.check(id) -> ProxyCheckResponse +- client.proxies.check(id, \*\*params) -> ProxyCheckResponse # Extensions diff --git a/src/kernel/resources/proxies.py b/src/kernel/resources/proxies.py index f259d6ed..bacdd570 100644 --- a/src/kernel/resources/proxies.py +++ b/src/kernel/resources/proxies.py @@ -6,7 +6,7 @@ import httpx -from ..types import proxy_create_params +from ..types import proxy_check_params, proxy_create_params from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, SequenceNotStr, omit, not_given from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property @@ -195,6 +195,7 @@ def check( self, id: str, *, + url: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -202,10 +203,27 @@ def check( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProxyCheckResponse: - """ - Run a health check on the proxy to verify it's working. + """Run a health check on the proxy to verify it's working. + + Optionally specify a URL + to test reachability against a specific target. For ISP and datacenter proxies, + this reliably tests whether the target site is reachable from the proxy's stable + exit IP. For residential and mobile proxies, the exit node varies between + requests, so this validates proxy configuration and connectivity rather than + guaranteeing site-specific reachability. Args: + url: An optional URL to test reachability against. If provided, the proxy check will + test connectivity to this URL instead of the default test URLs. Only HTTP and + HTTPS schemes are allowed, and the URL must resolve to a public IP address. For + ISP and datacenter proxies, the exit IP is stable, so a successful check + reliably indicates that subsequent browser sessions will reach the target site + with the same IP. For residential and mobile proxies, the exit node changes + between requests, so a successful check validates proxy configuration but does + not guarantee that a subsequent browser session will use the same exit IP or + reach the same site — it is useful for verifying credentials and connectivity, + not for predicting site-specific behavior. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -218,6 +236,7 @@ def check( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( path_template("/proxies/{id}/check", id=id), + body=maybe_transform({"url": url}, proxy_check_params.ProxyCheckParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -394,6 +413,7 @@ async def check( self, id: str, *, + url: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -401,10 +421,27 @@ async def check( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProxyCheckResponse: - """ - Run a health check on the proxy to verify it's working. + """Run a health check on the proxy to verify it's working. + + Optionally specify a URL + to test reachability against a specific target. For ISP and datacenter proxies, + this reliably tests whether the target site is reachable from the proxy's stable + exit IP. For residential and mobile proxies, the exit node varies between + requests, so this validates proxy configuration and connectivity rather than + guaranteeing site-specific reachability. Args: + url: An optional URL to test reachability against. If provided, the proxy check will + test connectivity to this URL instead of the default test URLs. Only HTTP and + HTTPS schemes are allowed, and the URL must resolve to a public IP address. For + ISP and datacenter proxies, the exit IP is stable, so a successful check + reliably indicates that subsequent browser sessions will reach the target site + with the same IP. For residential and mobile proxies, the exit node changes + between requests, so a successful check validates proxy configuration but does + not guarantee that a subsequent browser session will use the same exit IP or + reach the same site — it is useful for verifying credentials and connectivity, + not for predicting site-specific behavior. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -417,6 +454,7 @@ async def check( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( path_template("/proxies/{id}/check", id=id), + body=await async_maybe_transform({"url": url}, proxy_check_params.ProxyCheckParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 894342ac..0e740fba 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -20,6 +20,7 @@ from .app_list_params import AppListParams as AppListParams from .browser_pool_ref import BrowserPoolRef as BrowserPoolRef from .app_list_response import AppListResponse as AppListResponse +from .proxy_check_params import ProxyCheckParams as ProxyCheckParams from .browser_list_params import BrowserListParams as BrowserListParams from .browser_persistence import BrowserPersistence as BrowserPersistence from .credential_provider import CredentialProvider as CredentialProvider diff --git a/src/kernel/types/proxy_check_params.py b/src/kernel/types/proxy_check_params.py new file mode 100644 index 00000000..de3a8c3d --- /dev/null +++ b/src/kernel/types/proxy_check_params.py @@ -0,0 +1,23 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["ProxyCheckParams"] + + +class ProxyCheckParams(TypedDict, total=False): + url: str + """An optional URL to test reachability against. + + If provided, the proxy check will test connectivity to this URL instead of the + default test URLs. Only HTTP and HTTPS schemes are allowed, and the URL must + resolve to a public IP address. For ISP and datacenter proxies, the exit IP is + stable, so a successful check reliably indicates that subsequent browser + sessions will reach the target site with the same IP. For residential and mobile + proxies, the exit node changes between requests, so a successful check validates + proxy configuration but does not guarantee that a subsequent browser session + will use the same exit IP or reach the same site — it is useful for verifying + credentials and connectivity, not for predicting site-specific behavior. + """ diff --git a/tests/api_resources/test_proxies.py b/tests/api_resources/test_proxies.py index 9f107d2b..fd8080ee 100644 --- a/tests/api_resources/test_proxies.py +++ b/tests/api_resources/test_proxies.py @@ -184,7 +184,16 @@ def test_path_params_delete(self, client: Kernel) -> None: @parametrize def test_method_check(self, client: Kernel) -> None: proxy = client.proxies.check( - "id", + id="id", + ) + assert_matches_type(ProxyCheckResponse, proxy, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_check_with_all_params(self, client: Kernel) -> None: + proxy = client.proxies.check( + id="id", + url="url", ) assert_matches_type(ProxyCheckResponse, proxy, path=["response"]) @@ -192,7 +201,7 @@ def test_method_check(self, client: Kernel) -> None: @parametrize def test_raw_response_check(self, client: Kernel) -> None: response = client.proxies.with_raw_response.check( - "id", + id="id", ) assert response.is_closed is True @@ -204,7 +213,7 @@ def test_raw_response_check(self, client: Kernel) -> None: @parametrize def test_streaming_response_check(self, client: Kernel) -> None: with client.proxies.with_streaming_response.check( - "id", + id="id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -219,7 +228,7 @@ def test_streaming_response_check(self, client: Kernel) -> None: def test_path_params_check(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): client.proxies.with_raw_response.check( - "", + id="", ) @@ -390,7 +399,16 @@ async def test_path_params_delete(self, async_client: AsyncKernel) -> None: @parametrize async def test_method_check(self, async_client: AsyncKernel) -> None: proxy = await async_client.proxies.check( - "id", + id="id", + ) + assert_matches_type(ProxyCheckResponse, proxy, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_check_with_all_params(self, async_client: AsyncKernel) -> None: + proxy = await async_client.proxies.check( + id="id", + url="url", ) assert_matches_type(ProxyCheckResponse, proxy, path=["response"]) @@ -398,7 +416,7 @@ async def test_method_check(self, async_client: AsyncKernel) -> None: @parametrize async def test_raw_response_check(self, async_client: AsyncKernel) -> None: response = await async_client.proxies.with_raw_response.check( - "id", + id="id", ) assert response.is_closed is True @@ -410,7 +428,7 @@ async def test_raw_response_check(self, async_client: AsyncKernel) -> None: @parametrize async def test_streaming_response_check(self, async_client: AsyncKernel) -> None: async with async_client.proxies.with_streaming_response.check( - "id", + id="id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -425,5 +443,5 @@ async def test_streaming_response_check(self, async_client: AsyncKernel) -> None async def test_path_params_check(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): await async_client.proxies.with_raw_response.check( - "", + id="", ) From 95ffba6278e39ea1a969699188e6a0523a96cf36 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:58:08 +0000 Subject: [PATCH 354/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index fc0d7ff8..563004f2 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.45.0" + ".": "0.46.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index b8b563e9..10ca20a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.45.0" +version = "0.46.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 4483ae96..5b800100 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.45.0" # x-release-please-version +__version__ = "0.46.0" # x-release-please-version From f75abad2c956aafff4bc9edbf08b3025d70da7cd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 01:40:31 +0000 Subject: [PATCH 355/448] feat: Include login_url in managed auth connection response --- .stats.yml | 4 ++-- src/kernel/resources/auth/connections.py | 16 ++++++++++------ .../types/auth/connection_create_params.py | 5 ++++- src/kernel/types/auth/managed_auth.py | 3 +++ 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/.stats.yml b/.stats.yml index 4bc313ec..7f48513f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 104 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-f7024f4171c7c4ec558de1c27f338b1089ffddd0d2dbfdb9bb9f9c2abe8f47bf.yml -openapi_spec_hash: ced43682b49e73a2862f99b49abb4fcd +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-ac10847d991ef8ed89124b5550922cb5726af2b4a4c3396ee6ff82938302fc25.yml +openapi_spec_hash: 0d902563108fe2461708c05336eab40a config_hash: 16e4457a0bb26e98a335a1c2a572290a diff --git a/src/kernel/resources/auth/connections.py b/src/kernel/resources/auth/connections.py index c610da4c..fd28bd13 100644 --- a/src/kernel/resources/auth/connections.py +++ b/src/kernel/resources/auth/connections.py @@ -76,13 +76,15 @@ def create( ) -> ManagedAuth: """Creates an auth connection for a profile and domain combination. - Returns 409 - Conflict if an auth connection already exists for the given profile and domain. + If the provided + profile_name does not exist, it is created automatically. Returns 409 Conflict + if an auth connection already exists for the given profile and domain. Args: domain: Domain for authentication - profile_name: Name of the profile to manage authentication for + profile_name: Name of the profile to manage authentication for. If the profile does not exist, + it is created automatically. allowed_domains: Additional domains valid for this auth flow (besides the primary domain). Useful when login pages redirect to different domains. @@ -537,13 +539,15 @@ async def create( ) -> ManagedAuth: """Creates an auth connection for a profile and domain combination. - Returns 409 - Conflict if an auth connection already exists for the given profile and domain. + If the provided + profile_name does not exist, it is created automatically. Returns 409 Conflict + if an auth connection already exists for the given profile and domain. Args: domain: Domain for authentication - profile_name: Name of the profile to manage authentication for + profile_name: Name of the profile to manage authentication for. If the profile does not exist, + it is created automatically. allowed_domains: Additional domains valid for this auth flow (besides the primary domain). Useful when login pages redirect to different domains. diff --git a/src/kernel/types/auth/connection_create_params.py b/src/kernel/types/auth/connection_create_params.py index 89c787f2..b021994c 100644 --- a/src/kernel/types/auth/connection_create_params.py +++ b/src/kernel/types/auth/connection_create_params.py @@ -14,7 +14,10 @@ class ConnectionCreateParams(TypedDict, total=False): """Domain for authentication""" profile_name: Required[str] - """Name of the profile to manage authentication for""" + """Name of the profile to manage authentication for. + + If the profile does not exist, it is created automatically. + """ allowed_domains: SequenceNotStr[str] """Additional domains valid for this auth flow (besides the primary domain). diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py index 9f90bc5d..10dea637 100644 --- a/src/kernel/types/auth/managed_auth.py +++ b/src/kernel/types/auth/managed_auth.py @@ -215,6 +215,9 @@ class ManagedAuth(BaseModel): live_view_url: Optional[str] = None """Browser live view URL for debugging (present when flow in progress)""" + login_url: Optional[str] = None + """Optional login page URL to skip discovery""" + mfa_options: Optional[List[MfaOption]] = None """ MFA method options (present when flow_step=awaiting_input and MFA selection From 85c6631174e1b92857659f2b6ad352ae41eaec6c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 01:51:24 +0000 Subject: [PATCH 356/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 563004f2..141e7cde 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.46.0" + ".": "0.47.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 10ca20a1..c9fe0cd8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.46.0" +version = "0.47.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 5b800100..254e7e2d 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.46.0" # x-release-please-version +__version__ = "0.47.0" # x-release-please-version From d1fd4cd61def198a7801ac9cd2d039b98b9bf22f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 03:30:26 +0000 Subject: [PATCH 357/448] fix(client): preserve hardcoded query params when merging with user params --- src/kernel/_base_client.py | 4 ++++ tests/test_client.py | 48 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/kernel/_base_client.py b/src/kernel/_base_client.py index a3d47eaf..2599dc41 100644 --- a/src/kernel/_base_client.py +++ b/src/kernel/_base_client.py @@ -540,6 +540,10 @@ def _build_request( files = cast(HttpxRequestFiles, ForceMultipartDict()) prepared_url = self._prepare_url(options.url) + # preserve hard-coded query params from the url + if params and prepared_url.query: + params = {**dict(prepared_url.params.items()), **params} + prepared_url = prepared_url.copy_with(raw_path=prepared_url.raw_path.split(b"?", 1)[0]) if "_" in prepared_url.host: # work around https://github.com/encode/httpx/discussions/2880 kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")} diff --git a/tests/test_client.py b/tests/test_client.py index e5f4f849..e6005cbf 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -427,6 +427,30 @@ def test_default_query_option(self) -> None: client.close() + def test_hardcoded_query_params_in_url(self, client: Kernel) -> None: + request = client._build_request(FinalRequestOptions(method="get", url="/foo?beta=true")) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/foo?beta=true", + params={"limit": "10", "page": "abc"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true", "limit": "10", "page": "abc"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/files/a%2Fb?beta=true", + params={"limit": "10"}, + ) + ) + assert request.url.raw_path == b"/files/a%2Fb?beta=true&limit=10" + def test_request_extra_json(self, client: Kernel) -> None: request = client._build_request( FinalRequestOptions( @@ -1328,6 +1352,30 @@ async def test_default_query_option(self) -> None: await client.close() + async def test_hardcoded_query_params_in_url(self, async_client: AsyncKernel) -> None: + request = async_client._build_request(FinalRequestOptions(method="get", url="/foo?beta=true")) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true"} + + request = async_client._build_request( + FinalRequestOptions( + method="get", + url="/foo?beta=true", + params={"limit": "10", "page": "abc"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true", "limit": "10", "page": "abc"} + + request = async_client._build_request( + FinalRequestOptions( + method="get", + url="/files/a%2Fb?beta=true", + params={"limit": "10"}, + ) + ) + assert request.url.raw_path == b"/files/a%2Fb?beta=true&limit=10" + def test_request_extra_json(self, client: Kernel) -> None: request = client._build_request( FinalRequestOptions( From dcf33176733ae4a94a7e8ffb3c90cb859cf3a51a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 13:12:06 +0000 Subject: [PATCH 358/448] feat: [kernel-1116] add base_url field to browser session response --- .stats.yml | 4 ++-- src/kernel/types/browser_create_response.py | 3 +++ src/kernel/types/browser_list_response.py | 3 +++ src/kernel/types/browser_pool_acquire_response.py | 3 +++ src/kernel/types/browser_retrieve_response.py | 3 +++ src/kernel/types/browser_update_response.py | 3 +++ src/kernel/types/invocation_list_browsers_response.py | 3 +++ 7 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 7f48513f..b81754a7 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 104 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-ac10847d991ef8ed89124b5550922cb5726af2b4a4c3396ee6ff82938302fc25.yml -openapi_spec_hash: 0d902563108fe2461708c05336eab40a +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-aee09720882ec1d78f845fee6ceecb0466c264629e4edecd3230406dd06d7983.yml +openapi_spec_hash: 438da0d38d169897595f301d82fa7e2c config_hash: 16e4457a0bb26e98a335a1c2a572290a diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index d59a3d0d..9356bb05 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -35,6 +35,9 @@ class BrowserCreateResponse(BaseModel): webdriver_ws_url: str """Websocket URL for WebDriver BiDi connections to the browser session""" + base_url: Optional[str] = None + """Metro-API HTTP base URL for this browser session.""" + browser_live_view_url: Optional[str] = None """Remote URL for live viewing the browser session. diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index 708caa97..f3a88f29 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -35,6 +35,9 @@ class BrowserListResponse(BaseModel): webdriver_ws_url: str """Websocket URL for WebDriver BiDi connections to the browser session""" + base_url: Optional[str] = None + """Metro-API HTTP base URL for this browser session.""" + browser_live_view_url: Optional[str] = None """Remote URL for live viewing the browser session. diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py index 5ab52b58..064c405d 100644 --- a/src/kernel/types/browser_pool_acquire_response.py +++ b/src/kernel/types/browser_pool_acquire_response.py @@ -35,6 +35,9 @@ class BrowserPoolAcquireResponse(BaseModel): webdriver_ws_url: str """Websocket URL for WebDriver BiDi connections to the browser session""" + base_url: Optional[str] = None + """Metro-API HTTP base URL for this browser session.""" + browser_live_view_url: Optional[str] = None """Remote URL for live viewing the browser session. diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 221eab52..5b5a8913 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -35,6 +35,9 @@ class BrowserRetrieveResponse(BaseModel): webdriver_ws_url: str """Websocket URL for WebDriver BiDi connections to the browser session""" + base_url: Optional[str] = None + """Metro-API HTTP base URL for this browser session.""" + browser_live_view_url: Optional[str] = None """Remote URL for live viewing the browser session. diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py index c8a85c3b..188895ad 100644 --- a/src/kernel/types/browser_update_response.py +++ b/src/kernel/types/browser_update_response.py @@ -35,6 +35,9 @@ class BrowserUpdateResponse(BaseModel): webdriver_ws_url: str """Websocket URL for WebDriver BiDi connections to the browser session""" + base_url: Optional[str] = None + """Metro-API HTTP base URL for this browser session.""" + browser_live_view_url: Optional[str] = None """Remote URL for live viewing the browser session. diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py index a0fed9a2..23eda779 100644 --- a/src/kernel/types/invocation_list_browsers_response.py +++ b/src/kernel/types/invocation_list_browsers_response.py @@ -35,6 +35,9 @@ class Browser(BaseModel): webdriver_ws_url: str """Websocket URL for WebDriver BiDi connections to the browser session""" + base_url: Optional[str] = None + """Metro-API HTTP base URL for this browser session.""" + browser_live_view_url: Optional[str] = None """Remote URL for live viewing the browser session. From 667a9a565e08052ae9ef2ea0c2c818b7bd647948 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 20:48:21 +0000 Subject: [PATCH 359/448] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index b81754a7..d184bbd0 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 104 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-aee09720882ec1d78f845fee6ceecb0466c264629e4edecd3230406dd06d7983.yml -openapi_spec_hash: 438da0d38d169897595f301d82fa7e2c +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-49a1a92e00d1eb87e91e8527275cb0705fce2edea30e70fea745f134dd451fbd.yml +openapi_spec_hash: 3aa6ab6939790f538332054162fbdedc config_hash: 16e4457a0bb26e98a335a1c2a572290a From c967b4f04170b14fd97998916f31df3655096e89 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 02:14:32 +0000 Subject: [PATCH 360/448] chore: retrigger Stainless codegen for projects resource --- .stats.yml | 4 +- api.md | 29 + src/kernel/_client.py | 44 ++ src/kernel/resources/__init__.py | 14 + src/kernel/resources/projects/__init__.py | 33 + src/kernel/resources/projects/limits.py | 309 +++++++++ src/kernel/resources/projects/projects.py | 586 ++++++++++++++++++ src/kernel/types/__init__.py | 4 + src/kernel/types/project.py | 25 + src/kernel/types/project_create_params.py | 12 + src/kernel/types/project_list_params.py | 15 + src/kernel/types/project_update_params.py | 15 + src/kernel/types/projects/__init__.py | 6 + .../types/projects/limit_update_params.py | 34 + src/kernel/types/projects/project_limits.py | 33 + tests/api_resources/projects/__init__.py | 1 + tests/api_resources/projects/test_limits.py | 216 +++++++ tests/api_resources/test_projects.py | 439 +++++++++++++ 18 files changed, 1817 insertions(+), 2 deletions(-) create mode 100644 src/kernel/resources/projects/__init__.py create mode 100644 src/kernel/resources/projects/limits.py create mode 100644 src/kernel/resources/projects/projects.py create mode 100644 src/kernel/types/project.py create mode 100644 src/kernel/types/project_create_params.py create mode 100644 src/kernel/types/project_list_params.py create mode 100644 src/kernel/types/project_update_params.py create mode 100644 src/kernel/types/projects/__init__.py create mode 100644 src/kernel/types/projects/limit_update_params.py create mode 100644 src/kernel/types/projects/project_limits.py create mode 100644 tests/api_resources/projects/__init__.py create mode 100644 tests/api_resources/projects/test_limits.py create mode 100644 tests/api_resources/test_projects.py diff --git a/.stats.yml b/.stats.yml index d184bbd0..db7b03c8 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 104 +configured_endpoints: 111 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-49a1a92e00d1eb87e91e8527275cb0705fce2edea30e70fea745f134dd451fbd.yml openapi_spec_hash: 3aa6ab6939790f538332054162fbdedc -config_hash: 16e4457a0bb26e98a335a1c2a572290a +config_hash: 9818dd634f87b677410eefd013d7a179 diff --git a/api.md b/api.md index 5e43066c..96c90c46 100644 --- a/api.md +++ b/api.md @@ -341,6 +341,35 @@ Methods: - client.credentials.delete(id_or_name) -> None - client.credentials.totp_code(id_or_name) -> CredentialTotpCodeResponse +# Projects + +Types: + +```python +from kernel.types import CreateProjectRequest, Project, UpdateProjectRequest +``` + +Methods: + +- client.projects.create(\*\*params) -> Project +- client.projects.retrieve(id) -> Project +- client.projects.update(id, \*\*params) -> Project +- client.projects.list(\*\*params) -> SyncOffsetPagination[Project] +- client.projects.delete(id) -> None + +## Limits + +Types: + +```python +from kernel.types.projects import ProjectLimits, UpdateProjectLimitsRequest +``` + +Methods: + +- client.projects.limits.retrieve(id) -> ProjectLimits +- client.projects.limits.update(id, \*\*params) -> ProjectLimits + # CredentialProviders Types: diff --git a/src/kernel/_client.py b/src/kernel/_client.py index 84fc48dd..75fe4b64 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -37,6 +37,7 @@ proxies, browsers, profiles, + projects, extensions, credentials, deployments, @@ -54,6 +55,7 @@ from .resources.invocations import InvocationsResource, AsyncInvocationsResource from .resources.browser_pools import BrowserPoolsResource, AsyncBrowserPoolsResource from .resources.browsers.browsers import BrowsersResource, AsyncBrowsersResource + from .resources.projects.projects import ProjectsResource, AsyncProjectsResource from .resources.credential_providers import CredentialProvidersResource, AsyncCredentialProvidersResource __all__ = [ @@ -222,6 +224,13 @@ def credentials(self) -> CredentialsResource: return CredentialsResource(self) + @cached_property + def projects(self) -> ProjectsResource: + """Create and manage projects for resource isolation within an organization.""" + from .resources.projects import ProjectsResource + + return ProjectsResource(self) + @cached_property def credential_providers(self) -> CredentialProvidersResource: """Configure external credential providers like 1Password.""" @@ -492,6 +501,13 @@ def credentials(self) -> AsyncCredentialsResource: return AsyncCredentialsResource(self) + @cached_property + def projects(self) -> AsyncProjectsResource: + """Create and manage projects for resource isolation within an organization.""" + from .resources.projects import AsyncProjectsResource + + return AsyncProjectsResource(self) + @cached_property def credential_providers(self) -> AsyncCredentialProvidersResource: """Configure external credential providers like 1Password.""" @@ -689,6 +705,13 @@ def credentials(self) -> credentials.CredentialsResourceWithRawResponse: return CredentialsResourceWithRawResponse(self._client.credentials) + @cached_property + def projects(self) -> projects.ProjectsResourceWithRawResponse: + """Create and manage projects for resource isolation within an organization.""" + from .resources.projects import ProjectsResourceWithRawResponse + + return ProjectsResourceWithRawResponse(self._client.projects) + @cached_property def credential_providers(self) -> credential_providers.CredentialProvidersResourceWithRawResponse: """Configure external credential providers like 1Password.""" @@ -772,6 +795,13 @@ def credentials(self) -> credentials.AsyncCredentialsResourceWithRawResponse: return AsyncCredentialsResourceWithRawResponse(self._client.credentials) + @cached_property + def projects(self) -> projects.AsyncProjectsResourceWithRawResponse: + """Create and manage projects for resource isolation within an organization.""" + from .resources.projects import AsyncProjectsResourceWithRawResponse + + return AsyncProjectsResourceWithRawResponse(self._client.projects) + @cached_property def credential_providers(self) -> credential_providers.AsyncCredentialProvidersResourceWithRawResponse: """Configure external credential providers like 1Password.""" @@ -855,6 +885,13 @@ def credentials(self) -> credentials.CredentialsResourceWithStreamingResponse: return CredentialsResourceWithStreamingResponse(self._client.credentials) + @cached_property + def projects(self) -> projects.ProjectsResourceWithStreamingResponse: + """Create and manage projects for resource isolation within an organization.""" + from .resources.projects import ProjectsResourceWithStreamingResponse + + return ProjectsResourceWithStreamingResponse(self._client.projects) + @cached_property def credential_providers(self) -> credential_providers.CredentialProvidersResourceWithStreamingResponse: """Configure external credential providers like 1Password.""" @@ -938,6 +975,13 @@ def credentials(self) -> credentials.AsyncCredentialsResourceWithStreamingRespon return AsyncCredentialsResourceWithStreamingResponse(self._client.credentials) + @cached_property + def projects(self) -> projects.AsyncProjectsResourceWithStreamingResponse: + """Create and manage projects for resource isolation within an organization.""" + from .resources.projects import AsyncProjectsResourceWithStreamingResponse + + return AsyncProjectsResourceWithStreamingResponse(self._client.projects) + @cached_property def credential_providers(self) -> credential_providers.AsyncCredentialProvidersResourceWithStreamingResponse: """Configure external credential providers like 1Password.""" diff --git a/src/kernel/resources/__init__.py b/src/kernel/resources/__init__.py index 4896e79b..f078a03b 100644 --- a/src/kernel/resources/__init__.py +++ b/src/kernel/resources/__init__.py @@ -40,6 +40,14 @@ ProfilesResourceWithStreamingResponse, AsyncProfilesResourceWithStreamingResponse, ) +from .projects import ( + ProjectsResource, + AsyncProjectsResource, + ProjectsResourceWithRawResponse, + AsyncProjectsResourceWithRawResponse, + ProjectsResourceWithStreamingResponse, + AsyncProjectsResourceWithStreamingResponse, +) from .extensions import ( ExtensionsResource, AsyncExtensionsResource, @@ -150,6 +158,12 @@ "AsyncCredentialsResourceWithRawResponse", "CredentialsResourceWithStreamingResponse", "AsyncCredentialsResourceWithStreamingResponse", + "ProjectsResource", + "AsyncProjectsResource", + "ProjectsResourceWithRawResponse", + "AsyncProjectsResourceWithRawResponse", + "ProjectsResourceWithStreamingResponse", + "AsyncProjectsResourceWithStreamingResponse", "CredentialProvidersResource", "AsyncCredentialProvidersResource", "CredentialProvidersResourceWithRawResponse", diff --git a/src/kernel/resources/projects/__init__.py b/src/kernel/resources/projects/__init__.py new file mode 100644 index 00000000..41263893 --- /dev/null +++ b/src/kernel/resources/projects/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .limits import ( + LimitsResource, + AsyncLimitsResource, + LimitsResourceWithRawResponse, + AsyncLimitsResourceWithRawResponse, + LimitsResourceWithStreamingResponse, + AsyncLimitsResourceWithStreamingResponse, +) +from .projects import ( + ProjectsResource, + AsyncProjectsResource, + ProjectsResourceWithRawResponse, + AsyncProjectsResourceWithRawResponse, + ProjectsResourceWithStreamingResponse, + AsyncProjectsResourceWithStreamingResponse, +) + +__all__ = [ + "LimitsResource", + "AsyncLimitsResource", + "LimitsResourceWithRawResponse", + "AsyncLimitsResourceWithRawResponse", + "LimitsResourceWithStreamingResponse", + "AsyncLimitsResourceWithStreamingResponse", + "ProjectsResource", + "AsyncProjectsResource", + "ProjectsResourceWithRawResponse", + "AsyncProjectsResourceWithRawResponse", + "ProjectsResourceWithStreamingResponse", + "AsyncProjectsResourceWithStreamingResponse", +] diff --git a/src/kernel/resources/projects/limits.py b/src/kernel/resources/projects/limits.py new file mode 100644 index 00000000..eeff5930 --- /dev/null +++ b/src/kernel/resources/projects/limits.py @@ -0,0 +1,309 @@ +# 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 Body, Omit, Query, Headers, NotGiven, omit, not_given +from ..._utils import path_template, maybe_transform, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..._base_client import make_request_options +from ...types.projects import limit_update_params +from ...types.projects.project_limits import ProjectLimits + +__all__ = ["LimitsResource", "AsyncLimitsResource"] + + +class LimitsResource(SyncAPIResource): + """Create and manage projects for resource isolation within an organization.""" + + @cached_property + def with_raw_response(self) -> LimitsResourceWithRawResponse: + """ + 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/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return LimitsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> LimitsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response + """ + return LimitsResourceWithStreamingResponse(self) + + def retrieve( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ProjectLimits: + """Get the resource limit overrides for a project. + + Null values mean no + project-level cap (org limit applies). + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + path_template("/projects/{id}/limits", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProjectLimits, + ) + + def update( + self, + id: str, + *, + max_concurrent_invocations: Optional[int] | Omit = omit, + max_concurrent_sessions: Optional[int] | Omit = omit, + max_persistent_sessions: Optional[int] | Omit = omit, + max_pooled_sessions: Optional[int] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ProjectLimits: + """Update resource limit overrides for a project. + + Only fields present in the + request are modified. Set a field to 0 to remove that limit cap; omit a field to + leave it unchanged. + + Args: + max_concurrent_invocations: Maximum concurrent app invocations for this project. Set to 0 to remove the cap; + omit to leave unchanged. + + max_concurrent_sessions: Maximum concurrent browser sessions for this project. Set to 0 to remove the + cap; omit to leave unchanged. + + max_persistent_sessions: Maximum persistent browser sessions for this project. Set to 0 to remove the + cap; omit to leave unchanged. + + max_pooled_sessions: Maximum pooled sessions capacity for this project. Set to 0 to remove the cap; + omit to leave unchanged. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._patch( + path_template("/projects/{id}/limits", id=id), + body=maybe_transform( + { + "max_concurrent_invocations": max_concurrent_invocations, + "max_concurrent_sessions": max_concurrent_sessions, + "max_persistent_sessions": max_persistent_sessions, + "max_pooled_sessions": max_pooled_sessions, + }, + limit_update_params.LimitUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProjectLimits, + ) + + +class AsyncLimitsResource(AsyncAPIResource): + """Create and manage projects for resource isolation within an organization.""" + + @cached_property + def with_raw_response(self) -> AsyncLimitsResourceWithRawResponse: + """ + 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/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncLimitsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncLimitsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response + """ + return AsyncLimitsResourceWithStreamingResponse(self) + + async def retrieve( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ProjectLimits: + """Get the resource limit overrides for a project. + + Null values mean no + project-level cap (org limit applies). + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + path_template("/projects/{id}/limits", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProjectLimits, + ) + + async def update( + self, + id: str, + *, + max_concurrent_invocations: Optional[int] | Omit = omit, + max_concurrent_sessions: Optional[int] | Omit = omit, + max_persistent_sessions: Optional[int] | Omit = omit, + max_pooled_sessions: Optional[int] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ProjectLimits: + """Update resource limit overrides for a project. + + Only fields present in the + request are modified. Set a field to 0 to remove that limit cap; omit a field to + leave it unchanged. + + Args: + max_concurrent_invocations: Maximum concurrent app invocations for this project. Set to 0 to remove the cap; + omit to leave unchanged. + + max_concurrent_sessions: Maximum concurrent browser sessions for this project. Set to 0 to remove the + cap; omit to leave unchanged. + + max_persistent_sessions: Maximum persistent browser sessions for this project. Set to 0 to remove the + cap; omit to leave unchanged. + + max_pooled_sessions: Maximum pooled sessions capacity for this project. Set to 0 to remove the cap; + omit to leave unchanged. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._patch( + path_template("/projects/{id}/limits", id=id), + body=await async_maybe_transform( + { + "max_concurrent_invocations": max_concurrent_invocations, + "max_concurrent_sessions": max_concurrent_sessions, + "max_persistent_sessions": max_persistent_sessions, + "max_pooled_sessions": max_pooled_sessions, + }, + limit_update_params.LimitUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProjectLimits, + ) + + +class LimitsResourceWithRawResponse: + def __init__(self, limits: LimitsResource) -> None: + self._limits = limits + + self.retrieve = to_raw_response_wrapper( + limits.retrieve, + ) + self.update = to_raw_response_wrapper( + limits.update, + ) + + +class AsyncLimitsResourceWithRawResponse: + def __init__(self, limits: AsyncLimitsResource) -> None: + self._limits = limits + + self.retrieve = async_to_raw_response_wrapper( + limits.retrieve, + ) + self.update = async_to_raw_response_wrapper( + limits.update, + ) + + +class LimitsResourceWithStreamingResponse: + def __init__(self, limits: LimitsResource) -> None: + self._limits = limits + + self.retrieve = to_streamed_response_wrapper( + limits.retrieve, + ) + self.update = to_streamed_response_wrapper( + limits.update, + ) + + +class AsyncLimitsResourceWithStreamingResponse: + def __init__(self, limits: AsyncLimitsResource) -> None: + self._limits = limits + + self.retrieve = async_to_streamed_response_wrapper( + limits.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + limits.update, + ) diff --git a/src/kernel/resources/projects/projects.py b/src/kernel/resources/projects/projects.py new file mode 100644 index 00000000..b40dc02f --- /dev/null +++ b/src/kernel/resources/projects/projects.py @@ -0,0 +1,586 @@ +# 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 .limits import ( + LimitsResource, + AsyncLimitsResource, + LimitsResourceWithRawResponse, + AsyncLimitsResourceWithRawResponse, + LimitsResourceWithStreamingResponse, + AsyncLimitsResourceWithStreamingResponse, +) +from ...types import project_list_params, project_create_params, project_update_params +from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given +from ..._utils import path_template, maybe_transform, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ...pagination import SyncOffsetPagination, AsyncOffsetPagination +from ..._base_client import AsyncPaginator, make_request_options +from ...types.project import Project + +__all__ = ["ProjectsResource", "AsyncProjectsResource"] + + +class ProjectsResource(SyncAPIResource): + """Create and manage projects for resource isolation within an organization.""" + + @cached_property + def limits(self) -> LimitsResource: + """Create and manage projects for resource isolation within an organization.""" + return LimitsResource(self._client) + + @cached_property + def with_raw_response(self) -> ProjectsResourceWithRawResponse: + """ + 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/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return ProjectsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ProjectsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response + """ + return ProjectsResourceWithStreamingResponse(self) + + def create( + self, + *, + name: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Project: + """Create a new project within the authenticated organization. + + Requires a paid plan + and the projects feature flag. + + Args: + name: Project name (1-255 characters) + + 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( + "/projects", + body=maybe_transform({"name": name}, project_create_params.ProjectCreateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Project, + ) + + def retrieve( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Project: + """ + Get a project by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + path_template("/projects/{id}", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Project, + ) + + def update( + self, + id: str, + *, + name: str | Omit = omit, + status: Literal["active", "archived"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Project: + """ + Update a project's name or status. + + Args: + name: New project name + + status: New project status + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._patch( + path_template("/projects/{id}", id=id), + body=maybe_transform( + { + "name": name, + "status": status, + }, + project_update_params.ProjectUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Project, + ) + + def list( + self, + *, + limit: int | Omit = omit, + offset: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SyncOffsetPagination[Project]: + """ + List projects for the authenticated organization. + + Args: + limit: Maximum number of results to return + + offset: Number of results to skip + + 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_api_list( + "/projects", + page=SyncOffsetPagination[Project], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "offset": offset, + }, + project_list_params.ProjectListParams, + ), + ), + model=Project, + ) + + def delete( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """Soft-delete a project. + + The project must be empty (no active resources). + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + path_template("/projects/{id}", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class AsyncProjectsResource(AsyncAPIResource): + """Create and manage projects for resource isolation within an organization.""" + + @cached_property + def limits(self) -> AsyncLimitsResource: + """Create and manage projects for resource isolation within an organization.""" + return AsyncLimitsResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncProjectsResourceWithRawResponse: + """ + 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/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncProjectsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncProjectsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response + """ + return AsyncProjectsResourceWithStreamingResponse(self) + + async def create( + self, + *, + name: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Project: + """Create a new project within the authenticated organization. + + Requires a paid plan + and the projects feature flag. + + Args: + name: Project name (1-255 characters) + + 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( + "/projects", + body=await async_maybe_transform({"name": name}, project_create_params.ProjectCreateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Project, + ) + + async def retrieve( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Project: + """ + Get a project by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + path_template("/projects/{id}", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Project, + ) + + async def update( + self, + id: str, + *, + name: str | Omit = omit, + status: Literal["active", "archived"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Project: + """ + Update a project's name or status. + + Args: + name: New project name + + status: New project status + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._patch( + path_template("/projects/{id}", id=id), + body=await async_maybe_transform( + { + "name": name, + "status": status, + }, + project_update_params.ProjectUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Project, + ) + + def list( + self, + *, + limit: int | Omit = omit, + offset: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[Project, AsyncOffsetPagination[Project]]: + """ + List projects for the authenticated organization. + + Args: + limit: Maximum number of results to return + + offset: Number of results to skip + + 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_api_list( + "/projects", + page=AsyncOffsetPagination[Project], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "offset": offset, + }, + project_list_params.ProjectListParams, + ), + ), + model=Project, + ) + + async def delete( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """Soft-delete a project. + + The project must be empty (no active resources). + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + path_template("/projects/{id}", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class ProjectsResourceWithRawResponse: + def __init__(self, projects: ProjectsResource) -> None: + self._projects = projects + + self.create = to_raw_response_wrapper( + projects.create, + ) + self.retrieve = to_raw_response_wrapper( + projects.retrieve, + ) + self.update = to_raw_response_wrapper( + projects.update, + ) + self.list = to_raw_response_wrapper( + projects.list, + ) + self.delete = to_raw_response_wrapper( + projects.delete, + ) + + @cached_property + def limits(self) -> LimitsResourceWithRawResponse: + """Create and manage projects for resource isolation within an organization.""" + return LimitsResourceWithRawResponse(self._projects.limits) + + +class AsyncProjectsResourceWithRawResponse: + def __init__(self, projects: AsyncProjectsResource) -> None: + self._projects = projects + + self.create = async_to_raw_response_wrapper( + projects.create, + ) + self.retrieve = async_to_raw_response_wrapper( + projects.retrieve, + ) + self.update = async_to_raw_response_wrapper( + projects.update, + ) + self.list = async_to_raw_response_wrapper( + projects.list, + ) + self.delete = async_to_raw_response_wrapper( + projects.delete, + ) + + @cached_property + def limits(self) -> AsyncLimitsResourceWithRawResponse: + """Create and manage projects for resource isolation within an organization.""" + return AsyncLimitsResourceWithRawResponse(self._projects.limits) + + +class ProjectsResourceWithStreamingResponse: + def __init__(self, projects: ProjectsResource) -> None: + self._projects = projects + + self.create = to_streamed_response_wrapper( + projects.create, + ) + self.retrieve = to_streamed_response_wrapper( + projects.retrieve, + ) + self.update = to_streamed_response_wrapper( + projects.update, + ) + self.list = to_streamed_response_wrapper( + projects.list, + ) + self.delete = to_streamed_response_wrapper( + projects.delete, + ) + + @cached_property + def limits(self) -> LimitsResourceWithStreamingResponse: + """Create and manage projects for resource isolation within an organization.""" + return LimitsResourceWithStreamingResponse(self._projects.limits) + + +class AsyncProjectsResourceWithStreamingResponse: + def __init__(self, projects: AsyncProjectsResource) -> None: + self._projects = projects + + self.create = async_to_streamed_response_wrapper( + projects.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + projects.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + projects.update, + ) + self.list = async_to_streamed_response_wrapper( + projects.list, + ) + self.delete = async_to_streamed_response_wrapper( + projects.delete, + ) + + @cached_property + def limits(self) -> AsyncLimitsResourceWithStreamingResponse: + """Create and manage projects for resource isolation within an organization.""" + return AsyncLimitsResourceWithStreamingResponse(self._projects.limits) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 0e740fba..91e68d63 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -14,6 +14,7 @@ BrowserExtension as BrowserExtension, ) from .profile import Profile as Profile +from .project import Project as Project from .credential import Credential as Credential from .browser_pool import BrowserPool as BrowserPool from .browser_usage import BrowserUsage as BrowserUsage @@ -25,6 +26,7 @@ from .browser_persistence import BrowserPersistence as BrowserPersistence from .credential_provider import CredentialProvider as CredentialProvider from .profile_list_params import ProfileListParams as ProfileListParams +from .project_list_params import ProjectListParams as ProjectListParams from .proxy_create_params import ProxyCreateParams as ProxyCreateParams from .proxy_list_response import ProxyListResponse as ProxyListResponse from .proxy_check_response import ProxyCheckResponse as ProxyCheckResponse @@ -33,6 +35,8 @@ from .browser_list_response import BrowserListResponse as BrowserListResponse from .browser_update_params import BrowserUpdateParams as BrowserUpdateParams from .profile_create_params import ProfileCreateParams as ProfileCreateParams +from .project_create_params import ProjectCreateParams as ProjectCreateParams +from .project_update_params import ProjectUpdateParams as ProjectUpdateParams from .proxy_create_response import ProxyCreateResponse as ProxyCreateResponse from .credential_list_params import CredentialListParams as CredentialListParams from .deployment_list_params import DeploymentListParams as DeploymentListParams diff --git a/src/kernel/types/project.py b/src/kernel/types/project.py new file mode 100644 index 00000000..db2a377c --- /dev/null +++ b/src/kernel/types/project.py @@ -0,0 +1,25 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from datetime import datetime +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["Project"] + + +class Project(BaseModel): + id: str + """Unique project identifier""" + + created_at: datetime + """When the project was created""" + + name: str + """Project name""" + + status: Literal["active", "archived"] + """Project status""" + + updated_at: datetime + """When the project was last updated""" diff --git a/src/kernel/types/project_create_params.py b/src/kernel/types/project_create_params.py new file mode 100644 index 00000000..99a59864 --- /dev/null +++ b/src/kernel/types/project_create_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["ProjectCreateParams"] + + +class ProjectCreateParams(TypedDict, total=False): + name: Required[str] + """Project name (1-255 characters)""" diff --git a/src/kernel/types/project_list_params.py b/src/kernel/types/project_list_params.py new file mode 100644 index 00000000..ea10f073 --- /dev/null +++ b/src/kernel/types/project_list_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["ProjectListParams"] + + +class ProjectListParams(TypedDict, total=False): + limit: int + """Maximum number of results to return""" + + offset: int + """Number of results to skip""" diff --git a/src/kernel/types/project_update_params.py b/src/kernel/types/project_update_params.py new file mode 100644 index 00000000..ea7de900 --- /dev/null +++ b/src/kernel/types/project_update_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, TypedDict + +__all__ = ["ProjectUpdateParams"] + + +class ProjectUpdateParams(TypedDict, total=False): + name: str + """New project name""" + + status: Literal["active", "archived"] + """New project status""" diff --git a/src/kernel/types/projects/__init__.py b/src/kernel/types/projects/__init__.py new file mode 100644 index 00000000..acf030c8 --- /dev/null +++ b/src/kernel/types/projects/__init__.py @@ -0,0 +1,6 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .project_limits import ProjectLimits as ProjectLimits +from .limit_update_params import LimitUpdateParams as LimitUpdateParams diff --git a/src/kernel/types/projects/limit_update_params.py b/src/kernel/types/projects/limit_update_params.py new file mode 100644 index 00000000..6f0ec8ae --- /dev/null +++ b/src/kernel/types/projects/limit_update_params.py @@ -0,0 +1,34 @@ +# 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 TypedDict + +__all__ = ["LimitUpdateParams"] + + +class LimitUpdateParams(TypedDict, total=False): + max_concurrent_invocations: Optional[int] + """Maximum concurrent app invocations for this project. + + Set to 0 to remove the cap; omit to leave unchanged. + """ + + max_concurrent_sessions: Optional[int] + """Maximum concurrent browser sessions for this project. + + Set to 0 to remove the cap; omit to leave unchanged. + """ + + max_persistent_sessions: Optional[int] + """Maximum persistent browser sessions for this project. + + Set to 0 to remove the cap; omit to leave unchanged. + """ + + max_pooled_sessions: Optional[int] + """Maximum pooled sessions capacity for this project. + + Set to 0 to remove the cap; omit to leave unchanged. + """ diff --git a/src/kernel/types/projects/project_limits.py b/src/kernel/types/projects/project_limits.py new file mode 100644 index 00000000..bf49b2a2 --- /dev/null +++ b/src/kernel/types/projects/project_limits.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = ["ProjectLimits"] + + +class ProjectLimits(BaseModel): + max_concurrent_invocations: Optional[int] = None + """Maximum concurrent app invocations for this project. + + Null means no project-level cap. + """ + + max_concurrent_sessions: Optional[int] = None + """Maximum concurrent browser sessions for this project. + + Null means no project-level cap. + """ + + max_persistent_sessions: Optional[int] = None + """Maximum persistent browser sessions for this project. + + Null means no project-level cap. + """ + + max_pooled_sessions: Optional[int] = None + """Maximum pooled sessions capacity for this project. + + Null means no project-level cap. + """ diff --git a/tests/api_resources/projects/__init__.py b/tests/api_resources/projects/__init__.py new file mode 100644 index 00000000..fd8019a9 --- /dev/null +++ b/tests/api_resources/projects/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/projects/test_limits.py b/tests/api_resources/projects/test_limits.py new file mode 100644 index 00000000..9df6a0df --- /dev/null +++ b/tests/api_resources/projects/test_limits.py @@ -0,0 +1,216 @@ +# 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 kernel import Kernel, AsyncKernel +from tests.utils import assert_matches_type +from kernel.types.projects import ProjectLimits + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestLimits: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve(self, client: Kernel) -> None: + limit = client.projects.limits.retrieve( + "id", + ) + assert_matches_type(ProjectLimits, limit, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: Kernel) -> None: + response = client.projects.limits.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + limit = response.parse() + assert_matches_type(ProjectLimits, limit, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: Kernel) -> None: + with client.projects.limits.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + limit = response.parse() + assert_matches_type(ProjectLimits, limit, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_retrieve(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.projects.limits.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update(self, client: Kernel) -> None: + limit = client.projects.limits.update( + id="id", + ) + assert_matches_type(ProjectLimits, limit, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update_with_all_params(self, client: Kernel) -> None: + limit = client.projects.limits.update( + id="id", + max_concurrent_invocations=0, + max_concurrent_sessions=0, + max_persistent_sessions=0, + max_pooled_sessions=0, + ) + assert_matches_type(ProjectLimits, limit, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_update(self, client: Kernel) -> None: + response = client.projects.limits.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + limit = response.parse() + assert_matches_type(ProjectLimits, limit, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_update(self, client: Kernel) -> None: + with client.projects.limits.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + limit = response.parse() + assert_matches_type(ProjectLimits, limit, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_update(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.projects.limits.with_raw_response.update( + id="", + ) + + +class TestAsyncLimits: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve(self, async_client: AsyncKernel) -> None: + limit = await async_client.projects.limits.retrieve( + "id", + ) + assert_matches_type(ProjectLimits, limit, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: + response = await async_client.projects.limits.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + limit = await response.parse() + assert_matches_type(ProjectLimits, limit, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: + async with async_client.projects.limits.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + limit = await response.parse() + assert_matches_type(ProjectLimits, limit, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.projects.limits.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update(self, async_client: AsyncKernel) -> None: + limit = await async_client.projects.limits.update( + id="id", + ) + assert_matches_type(ProjectLimits, limit, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> None: + limit = await async_client.projects.limits.update( + id="id", + max_concurrent_invocations=0, + max_concurrent_sessions=0, + max_persistent_sessions=0, + max_pooled_sessions=0, + ) + assert_matches_type(ProjectLimits, limit, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_update(self, async_client: AsyncKernel) -> None: + response = await async_client.projects.limits.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + limit = await response.parse() + assert_matches_type(ProjectLimits, limit, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_update(self, async_client: AsyncKernel) -> None: + async with async_client.projects.limits.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + limit = await response.parse() + assert_matches_type(ProjectLimits, limit, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_update(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.projects.limits.with_raw_response.update( + id="", + ) diff --git a/tests/api_resources/test_projects.py b/tests/api_resources/test_projects.py new file mode 100644 index 00000000..488c191d --- /dev/null +++ b/tests/api_resources/test_projects.py @@ -0,0 +1,439 @@ +# 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 kernel import Kernel, AsyncKernel +from tests.utils import assert_matches_type +from kernel.types import Project +from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestProjects: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create(self, client: Kernel) -> None: + project = client.projects.create( + name="staging", + ) + assert_matches_type(Project, project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_create(self, client: Kernel) -> None: + response = client.projects.with_raw_response.create( + name="staging", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + project = response.parse() + assert_matches_type(Project, project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_create(self, client: Kernel) -> None: + with client.projects.with_streaming_response.create( + name="staging", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + project = response.parse() + assert_matches_type(Project, project, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve(self, client: Kernel) -> None: + project = client.projects.retrieve( + "id", + ) + assert_matches_type(Project, project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: Kernel) -> None: + response = client.projects.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + project = response.parse() + assert_matches_type(Project, project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: Kernel) -> None: + with client.projects.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + project = response.parse() + assert_matches_type(Project, project, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_retrieve(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.projects.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update(self, client: Kernel) -> None: + project = client.projects.update( + id="id", + ) + assert_matches_type(Project, project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update_with_all_params(self, client: Kernel) -> None: + project = client.projects.update( + id="id", + name="name", + status="active", + ) + assert_matches_type(Project, project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_update(self, client: Kernel) -> None: + response = client.projects.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + project = response.parse() + assert_matches_type(Project, project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_update(self, client: Kernel) -> None: + with client.projects.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + project = response.parse() + assert_matches_type(Project, project, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_update(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.projects.with_raw_response.update( + id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list(self, client: Kernel) -> None: + project = client.projects.list() + assert_matches_type(SyncOffsetPagination[Project], project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_with_all_params(self, client: Kernel) -> None: + project = client.projects.list( + limit=100, + offset=0, + ) + assert_matches_type(SyncOffsetPagination[Project], project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list(self, client: Kernel) -> None: + response = client.projects.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + project = response.parse() + assert_matches_type(SyncOffsetPagination[Project], project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list(self, client: Kernel) -> None: + with client.projects.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + project = response.parse() + assert_matches_type(SyncOffsetPagination[Project], project, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_delete(self, client: Kernel) -> None: + project = client.projects.delete( + "id", + ) + assert project is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_delete(self, client: Kernel) -> None: + response = client.projects.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + project = response.parse() + assert project is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_delete(self, client: Kernel) -> None: + with client.projects.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + project = response.parse() + assert project is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_delete(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.projects.with_raw_response.delete( + "", + ) + + +class TestAsyncProjects: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create(self, async_client: AsyncKernel) -> None: + project = await async_client.projects.create( + name="staging", + ) + assert_matches_type(Project, project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_create(self, async_client: AsyncKernel) -> None: + response = await async_client.projects.with_raw_response.create( + name="staging", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + project = await response.parse() + assert_matches_type(Project, project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: + async with async_client.projects.with_streaming_response.create( + name="staging", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + project = await response.parse() + assert_matches_type(Project, project, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve(self, async_client: AsyncKernel) -> None: + project = await async_client.projects.retrieve( + "id", + ) + assert_matches_type(Project, project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: + response = await async_client.projects.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + project = await response.parse() + assert_matches_type(Project, project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: + async with async_client.projects.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + project = await response.parse() + assert_matches_type(Project, project, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.projects.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update(self, async_client: AsyncKernel) -> None: + project = await async_client.projects.update( + id="id", + ) + assert_matches_type(Project, project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> None: + project = await async_client.projects.update( + id="id", + name="name", + status="active", + ) + assert_matches_type(Project, project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_update(self, async_client: AsyncKernel) -> None: + response = await async_client.projects.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + project = await response.parse() + assert_matches_type(Project, project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_update(self, async_client: AsyncKernel) -> None: + async with async_client.projects.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + project = await response.parse() + assert_matches_type(Project, project, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_update(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.projects.with_raw_response.update( + id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncKernel) -> None: + project = await async_client.projects.list() + assert_matches_type(AsyncOffsetPagination[Project], project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: + project = await async_client.projects.list( + limit=100, + offset=0, + ) + assert_matches_type(AsyncOffsetPagination[Project], project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncKernel) -> None: + response = await async_client.projects.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + project = await response.parse() + assert_matches_type(AsyncOffsetPagination[Project], project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: + async with async_client.projects.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + project = await response.parse() + assert_matches_type(AsyncOffsetPagination[Project], project, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_delete(self, async_client: AsyncKernel) -> None: + project = await async_client.projects.delete( + "id", + ) + assert project is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: + response = await async_client.projects.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + project = await response.parse() + assert project is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None: + async with async_client.projects.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + project = await response.parse() + assert project is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_delete(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.projects.with_raw_response.delete( + "", + ) From d47c60b1b96b1d32a76fe9026d326ec71824d36f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 13:46:25 +0000 Subject: [PATCH 361/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 141e7cde..ff661205 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.47.0" + ".": "0.48.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index c9fe0cd8..b26c2b0c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.47.0" +version = "0.48.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 254e7e2d..a9881de4 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.47.0" # x-release-please-version +__version__ = "0.48.0" # x-release-please-version From e90a3529b2b345ab21e342914eb3ab1bdd9de0ea Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:29:01 +0000 Subject: [PATCH 362/448] feat: Raise replay framerate limit from 20 to 60 fps --- .stats.yml | 4 ++-- src/kernel/resources/browsers/replays.py | 6 ++++-- src/kernel/types/browsers/replay_start_params.py | 5 ++++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.stats.yml b/.stats.yml index db7b03c8..481f7d4d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 111 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-49a1a92e00d1eb87e91e8527275cb0705fce2edea30e70fea745f134dd451fbd.yml -openapi_spec_hash: 3aa6ab6939790f538332054162fbdedc -config_hash: 9818dd634f87b677410eefd013d7a179 +openapi_spec_hash: 8670a9860c54682b158924e990d4de31 +config_hash: b12b028d6aa7564db0983135073805e3 diff --git a/src/kernel/resources/browsers/replays.py b/src/kernel/resources/browsers/replays.py index 2b20953a..8f56ea5f 100644 --- a/src/kernel/resources/browsers/replays.py +++ b/src/kernel/resources/browsers/replays.py @@ -139,7 +139,8 @@ def start( Start recording the browser session and return a replay ID. Args: - framerate: Recording framerate in fps. + framerate: Recording framerate in fps. Values above 20 require GPU to be enabled on the + browser session. max_duration_in_seconds: Maximum recording duration in seconds. @@ -315,7 +316,8 @@ async def start( Start recording the browser session and return a replay ID. Args: - framerate: Recording framerate in fps. + framerate: Recording framerate in fps. Values above 20 require GPU to be enabled on the + browser session. max_duration_in_seconds: Maximum recording duration in seconds. diff --git a/src/kernel/types/browsers/replay_start_params.py b/src/kernel/types/browsers/replay_start_params.py index d6683862..d77b6c49 100644 --- a/src/kernel/types/browsers/replay_start_params.py +++ b/src/kernel/types/browsers/replay_start_params.py @@ -9,7 +9,10 @@ class ReplayStartParams(TypedDict, total=False): framerate: int - """Recording framerate in fps.""" + """Recording framerate in fps. + + Values above 20 require GPU to be enabled on the browser session. + """ max_duration_in_seconds: int """Maximum recording duration in seconds.""" From 2123b2479a1f17b7673fdea162760d0aea6e1d3a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 17:32:38 +0000 Subject: [PATCH 363/448] feat: Neil/kernel 1180 fuzzy matching for browser pools --- .stats.yml | 4 ++-- src/kernel/resources/browsers/browsers.py | 4 ++-- src/kernel/types/browser_list_params.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.stats.yml b/.stats.yml index 481f7d4d..ad1b0f25 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 111 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-49a1a92e00d1eb87e91e8527275cb0705fce2edea30e70fea745f134dd451fbd.yml -openapi_spec_hash: 8670a9860c54682b158924e990d4de31 -config_hash: b12b028d6aa7564db0983135073805e3 +openapi_spec_hash: 0ffef6a95f9d9b1096180fc5e4c5b39c +config_hash: 9818dd634f87b677410eefd013d7a179 diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 078f31fb..1e20142b 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -367,7 +367,7 @@ def list( offset: Number of results to skip. Defaults to 0. - query: Search browsers by session ID, profile ID, or proxy ID. + query: Search browsers by session ID, profile ID, proxy ID, or pool name. status: Filter sessions by status. "active" returns only active sessions (default), "deleted" returns only soft-deleted sessions, "all" returns both. @@ -803,7 +803,7 @@ def list( offset: Number of results to skip. Defaults to 0. - query: Search browsers by session ID, profile ID, or proxy ID. + query: Search browsers by session ID, profile ID, proxy ID, or pool name. status: Filter sessions by status. "active" returns only active sessions (default), "deleted" returns only soft-deleted sessions, "all" returns both. diff --git a/src/kernel/types/browser_list_params.py b/src/kernel/types/browser_list_params.py index 4c858e1c..d659b8cb 100644 --- a/src/kernel/types/browser_list_params.py +++ b/src/kernel/types/browser_list_params.py @@ -22,7 +22,7 @@ class BrowserListParams(TypedDict, total=False): """Number of results to skip. Defaults to 0.""" query: str - """Search browsers by session ID, profile ID, or proxy ID.""" + """Search browsers by session ID, profile ID, proxy ID, or pool name.""" status: Literal["active", "deleted", "all"] """Filter sessions by status. From 726430704a212d7dcd440e1704c8456e480c86bc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 05:23:41 +0000 Subject: [PATCH 364/448] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index ad1b0f25..6b88334e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 111 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-49a1a92e00d1eb87e91e8527275cb0705fce2edea30e70fea745f134dd451fbd.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-7d048a0d07483d4fa8d1094f5ec172d1758f044b4e5ced1f41f92f1de8b47def.yml openapi_spec_hash: 0ffef6a95f9d9b1096180fc5e4c5b39c config_hash: 9818dd634f87b677410eefd013d7a179 From 519df263ff55e6c58b43162e820abfc86b836d47 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 05:25:50 +0000 Subject: [PATCH 365/448] fix: ensure file data are only sent as 1 parameter --- src/kernel/_utils/_utils.py | 5 +++-- tests/test_extract_files.py | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/kernel/_utils/_utils.py b/src/kernel/_utils/_utils.py index eec7f4a1..63b8cd60 100644 --- a/src/kernel/_utils/_utils.py +++ b/src/kernel/_utils/_utils.py @@ -86,8 +86,9 @@ def _extract_items( index += 1 if is_dict(obj): try: - # We are at the last entry in the path so we must remove the field - if (len(path)) == index: + # Remove the field if there are no more dict keys in the path, + # only "" traversal markers or end. + if all(p == "" for p in path[index:]): item = obj.pop(key) else: item = obj[key] diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py index e5cf4a1b..14a3932a 100644 --- a/tests/test_extract_files.py +++ b/tests/test_extract_files.py @@ -35,6 +35,15 @@ def test_multiple_files() -> None: assert query == {"documents": [{}, {}]} +def test_top_level_file_array() -> None: + query = {"files": [b"file one", b"file two"], "title": "hello"} + assert extract_files(query, paths=[["files", ""]]) == [ + ("files[]", b"file one"), + ("files[]", b"file two"), + ] + assert query == {"title": "hello"} + + @pytest.mark.parametrize( "query,paths,expected", [ From 58c4614f67835bac0b1865e900801a774c723d21 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:23:15 +0000 Subject: [PATCH 366/448] feat: add POST /browsers/{id}/curl and /curl/raw endpoints --- .stats.yml | 8 +- api.md | 2 + src/kernel/resources/browsers/browsers.py | 144 +++++++++++++++++++++- src/kernel/types/__init__.py | 2 + src/kernel/types/browser_curl_params.py | 28 +++++ src/kernel/types/browser_curl_response.py | 23 ++++ tests/api_resources/test_browsers.py | 121 ++++++++++++++++++ 7 files changed, 323 insertions(+), 5 deletions(-) create mode 100644 src/kernel/types/browser_curl_params.py create mode 100644 src/kernel/types/browser_curl_response.py diff --git a/.stats.yml b/.stats.yml index 6b88334e..f0ba92fd 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 111 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-7d048a0d07483d4fa8d1094f5ec172d1758f044b4e5ced1f41f92f1de8b47def.yml -openapi_spec_hash: 0ffef6a95f9d9b1096180fc5e4c5b39c -config_hash: 9818dd634f87b677410eefd013d7a179 +configured_endpoints: 112 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-930823e8b25b4644b74098ad5479840f64a329321aa236460f8a9562ae9051bf.yml +openapi_spec_hash: 9f868e67df8fd2fec8d8fc3eb5ba0b26 +config_hash: 08d55086449943a8fec212b870061a3f diff --git a/api.md b/api.md index 96c90c46..3dea16a3 100644 --- a/api.md +++ b/api.md @@ -88,6 +88,7 @@ from kernel.types import ( BrowserRetrieveResponse, BrowserUpdateResponse, BrowserListResponse, + BrowserCurlResponse, ) ``` @@ -98,6 +99,7 @@ Methods: - client.browsers.update(id, \*\*params) -> BrowserUpdateResponse - client.browsers.list(\*\*params) -> SyncOffsetPagination[BrowserListResponse] - client.browsers.delete(\*\*params) -> None +- client.browsers.curl(id, \*\*params) -> BrowserCurlResponse - client.browsers.delete_by_id(id) -> None - client.browsers.load_extensions(id, \*\*params) -> None diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 1e20142b..3fc1049e 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -3,7 +3,7 @@ from __future__ import annotations import typing_extensions -from typing import Mapping, Iterable, Optional, cast +from typing import Dict, Mapping, Iterable, Optional, cast from typing_extensions import Literal import httpx @@ -25,6 +25,7 @@ AsyncFsResourceWithStreamingResponse, ) from ...types import ( + browser_curl_params, browser_list_params, browser_create_params, browser_delete_params, @@ -76,6 +77,7 @@ ) from ...pagination import SyncOffsetPagination, AsyncOffsetPagination from ..._base_client import AsyncPaginator, make_request_options +from ...types.browser_curl_response import BrowserCurlResponse from ...types.browser_list_response import BrowserListResponse from ...types.browser_create_response import BrowserCreateResponse from ...types.browser_update_response import BrowserUpdateResponse @@ -443,6 +445,70 @@ def delete( cast_to=NoneType, ) + def curl( + self, + id: str, + *, + url: str, + body: str | Omit = omit, + headers: Dict[str, str] | Omit = omit, + method: Literal["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"] | Omit = omit, + response_encoding: Literal["utf8", "base64"] | Omit = omit, + timeout_ms: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> BrowserCurlResponse: + """ + Sends an HTTP request through Chrome's HTTP request stack, inheriting the + browser's TLS fingerprint, cookies, proxy configuration, and headers. Returns a + structured JSON response with status, headers, body, and timing. + + Args: + url: Target URL (must be http or https). + + body: Request body (for POST/PUT/PATCH). + + headers: Custom headers merged with browser defaults. + + method: HTTP method. + + response_encoding: Encoding for the response body. Use base64 for binary content. + + timeout_ms: Request timeout in milliseconds. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + path_template("/browsers/{id}/curl", id=id), + body=maybe_transform( + { + "url": url, + "body": body, + "headers": headers, + "method": method, + "response_encoding": response_encoding, + "timeout_ms": timeout_ms, + }, + browser_curl_params.BrowserCurlParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BrowserCurlResponse, + ) + def delete_by_id( self, id: str, @@ -881,6 +947,70 @@ async def delete( cast_to=NoneType, ) + async def curl( + self, + id: str, + *, + url: str, + body: str | Omit = omit, + headers: Dict[str, str] | Omit = omit, + method: Literal["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"] | Omit = omit, + response_encoding: Literal["utf8", "base64"] | Omit = omit, + timeout_ms: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> BrowserCurlResponse: + """ + Sends an HTTP request through Chrome's HTTP request stack, inheriting the + browser's TLS fingerprint, cookies, proxy configuration, and headers. Returns a + structured JSON response with status, headers, body, and timing. + + Args: + url: Target URL (must be http or https). + + body: Request body (for POST/PUT/PATCH). + + headers: Custom headers merged with browser defaults. + + method: HTTP method. + + response_encoding: Encoding for the response body. Use base64 for binary content. + + timeout_ms: Request timeout in milliseconds. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + path_template("/browsers/{id}/curl", id=id), + body=await async_maybe_transform( + { + "url": url, + "body": body, + "headers": headers, + "method": method, + "response_encoding": response_encoding, + "timeout_ms": timeout_ms, + }, + browser_curl_params.BrowserCurlParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BrowserCurlResponse, + ) + async def delete_by_id( self, id: str, @@ -983,6 +1113,9 @@ def __init__(self, browsers: BrowsersResource) -> None: browsers.delete, # pyright: ignore[reportDeprecated], ) ) + self.curl = to_raw_response_wrapper( + browsers.curl, + ) self.delete_by_id = to_raw_response_wrapper( browsers.delete_by_id, ) @@ -1041,6 +1174,9 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None: browsers.delete, # pyright: ignore[reportDeprecated], ) ) + self.curl = async_to_raw_response_wrapper( + browsers.curl, + ) self.delete_by_id = async_to_raw_response_wrapper( browsers.delete_by_id, ) @@ -1099,6 +1235,9 @@ def __init__(self, browsers: BrowsersResource) -> None: browsers.delete, # pyright: ignore[reportDeprecated], ) ) + self.curl = to_streamed_response_wrapper( + browsers.curl, + ) self.delete_by_id = to_streamed_response_wrapper( browsers.delete_by_id, ) @@ -1157,6 +1296,9 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None: browsers.delete, # pyright: ignore[reportDeprecated], ) ) + self.curl = async_to_streamed_response_wrapper( + browsers.curl, + ) self.delete_by_id = async_to_streamed_response_wrapper( browsers.delete_by_id, ) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 91e68d63..3838047e 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -22,6 +22,7 @@ from .browser_pool_ref import BrowserPoolRef as BrowserPoolRef from .app_list_response import AppListResponse as AppListResponse from .proxy_check_params import ProxyCheckParams as ProxyCheckParams +from .browser_curl_params import BrowserCurlParams as BrowserCurlParams from .browser_list_params import BrowserListParams as BrowserListParams from .browser_persistence import BrowserPersistence as BrowserPersistence from .credential_provider import CredentialProvider as CredentialProvider @@ -31,6 +32,7 @@ from .proxy_list_response import ProxyListResponse as ProxyListResponse from .proxy_check_response import ProxyCheckResponse as ProxyCheckResponse from .browser_create_params import BrowserCreateParams as BrowserCreateParams +from .browser_curl_response import BrowserCurlResponse as BrowserCurlResponse from .browser_delete_params import BrowserDeleteParams as BrowserDeleteParams from .browser_list_response import BrowserListResponse as BrowserListResponse from .browser_update_params import BrowserUpdateParams as BrowserUpdateParams diff --git a/src/kernel/types/browser_curl_params.py b/src/kernel/types/browser_curl_params.py new file mode 100644 index 00000000..750bd6d3 --- /dev/null +++ b/src/kernel/types/browser_curl_params.py @@ -0,0 +1,28 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["BrowserCurlParams"] + + +class BrowserCurlParams(TypedDict, total=False): + url: Required[str] + """Target URL (must be http or https).""" + + body: str + """Request body (for POST/PUT/PATCH).""" + + headers: Dict[str, str] + """Custom headers merged with browser defaults.""" + + method: Literal["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"] + """HTTP method.""" + + response_encoding: Literal["utf8", "base64"] + """Encoding for the response body. Use base64 for binary content.""" + + timeout_ms: int + """Request timeout in milliseconds.""" diff --git a/src/kernel/types/browser_curl_response.py b/src/kernel/types/browser_curl_response.py new file mode 100644 index 00000000..1b288e44 --- /dev/null +++ b/src/kernel/types/browser_curl_response.py @@ -0,0 +1,23 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, List + +from .._models import BaseModel + +__all__ = ["BrowserCurlResponse"] + + +class BrowserCurlResponse(BaseModel): + """Structured response from the browser curl request.""" + + body: str + """Response body (UTF-8 string or base64 depending on request).""" + + duration_ms: int + """Total request duration in milliseconds.""" + + headers: Dict[str, List[str]] + """Response headers (multi-value).""" + + status: int + """HTTP status code from target.""" diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index 72c7b023..96742c72 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -10,6 +10,7 @@ from kernel import Kernel, AsyncKernel from tests.utils import assert_matches_type from kernel.types import ( + BrowserCurlResponse, BrowserListResponse, BrowserCreateResponse, BrowserUpdateResponse, @@ -276,6 +277,66 @@ def test_streaming_response_delete(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_curl(self, client: Kernel) -> None: + browser = client.browsers.curl( + id="id", + url="url", + ) + assert_matches_type(BrowserCurlResponse, browser, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_curl_with_all_params(self, client: Kernel) -> None: + browser = client.browsers.curl( + id="id", + url="url", + body="body", + headers={"foo": "string"}, + method="GET", + response_encoding="utf8", + timeout_ms=1000, + ) + assert_matches_type(BrowserCurlResponse, browser, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_curl(self, client: Kernel) -> None: + response = client.browsers.with_raw_response.curl( + id="id", + url="url", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser = response.parse() + assert_matches_type(BrowserCurlResponse, browser, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_curl(self, client: Kernel) -> None: + with client.browsers.with_streaming_response.curl( + id="id", + url="url", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser = response.parse() + assert_matches_type(BrowserCurlResponse, browser, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_curl(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.with_raw_response.curl( + id="", + url="url", + ) + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_delete_by_id(self, client: Kernel) -> None: @@ -641,6 +702,66 @@ async def test_streaming_response_delete(self, async_client: AsyncKernel) -> Non assert cast(Any, response.is_closed) is True + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_curl(self, async_client: AsyncKernel) -> None: + browser = await async_client.browsers.curl( + id="id", + url="url", + ) + assert_matches_type(BrowserCurlResponse, browser, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_curl_with_all_params(self, async_client: AsyncKernel) -> None: + browser = await async_client.browsers.curl( + id="id", + url="url", + body="body", + headers={"foo": "string"}, + method="GET", + response_encoding="utf8", + timeout_ms=1000, + ) + assert_matches_type(BrowserCurlResponse, browser, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_curl(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.with_raw_response.curl( + id="id", + url="url", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser = await response.parse() + assert_matches_type(BrowserCurlResponse, browser, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_curl(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.with_streaming_response.curl( + id="id", + url="url", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser = await response.parse() + assert_matches_type(BrowserCurlResponse, browser, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_curl(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.with_raw_response.curl( + id="", + url="url", + ) + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_delete_by_id(self, async_client: AsyncKernel) -> None: From 018748ae1a3899d0fe5cc45122c20274a76cf968 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 05:19:23 +0000 Subject: [PATCH 367/448] perf(client): optimize file structure copying in multipart requests --- src/kernel/_files.py | 56 ++++++++++++- src/kernel/_utils/__init__.py | 1 - src/kernel/_utils/_utils.py | 15 ---- src/kernel/resources/browsers/browsers.py | 7 +- src/kernel/resources/browsers/fs/fs.py | 18 +++-- src/kernel/resources/deployments.py | 13 +-- src/kernel/resources/extensions.py | 13 +-- tests/test_deepcopy.py | 58 ------------- tests/test_files.py | 99 ++++++++++++++++++++++- 9 files changed, 181 insertions(+), 99 deletions(-) delete mode 100644 tests/test_deepcopy.py diff --git a/src/kernel/_files.py b/src/kernel/_files.py index bbef8bfb..f30ac58a 100644 --- a/src/kernel/_files.py +++ b/src/kernel/_files.py @@ -3,8 +3,8 @@ import io import os import pathlib -from typing import overload -from typing_extensions import TypeGuard +from typing import Sequence, cast, overload +from typing_extensions import TypeVar, TypeGuard import anyio @@ -17,7 +17,9 @@ HttpxFileContent, HttpxRequestFiles, ) -from ._utils import is_tuple_t, is_mapping_t, is_sequence_t +from ._utils import is_list, is_mapping, is_tuple_t, is_mapping_t, is_sequence_t + +_T = TypeVar("_T") def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]: @@ -121,3 +123,51 @@ async def async_read_file_content(file: FileContent) -> HttpxFileContent: return await anyio.Path(file).read_bytes() return file + + +def deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]]) -> _T: + """Copy only the containers along the given paths. + + Used to guard against mutation by extract_files without copying the entire structure. + Only dicts and lists that lie on a path are copied; everything else + is returned by reference. + + For example, given paths=[["foo", "files", "file"]] and the structure: + { + "foo": { + "bar": {"baz": {}}, + "files": {"file": } + } + } + The root dict, "foo", and "files" are copied (they lie on the path). + "bar" and "baz" are returned by reference (off the path). + """ + return _deepcopy_with_paths(item, paths, 0) + + +def _deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]], index: int) -> _T: + if not paths: + return item + if is_mapping(item): + key_to_paths: dict[str, list[Sequence[str]]] = {} + for path in paths: + if index < len(path): + key_to_paths.setdefault(path[index], []).append(path) + + # if no path continues through this mapping, it won't be mutated and copying it is redundant + if not key_to_paths: + return item + + result = dict(item) + for key, subpaths in key_to_paths.items(): + if key in result: + result[key] = _deepcopy_with_paths(result[key], subpaths, index + 1) + return cast(_T, result) + if is_list(item): + array_paths = [path for path in paths if index < len(path) and path[index] == ""] + + # if no path expects a list here, nothing will be mutated inside it - return by reference + if not array_paths: + return cast(_T, item) + return cast(_T, [_deepcopy_with_paths(entry, array_paths, index + 1) for entry in item]) + return item diff --git a/src/kernel/_utils/__init__.py b/src/kernel/_utils/__init__.py index 10cb66d2..1c090e51 100644 --- a/src/kernel/_utils/__init__.py +++ b/src/kernel/_utils/__init__.py @@ -24,7 +24,6 @@ coerce_integer as coerce_integer, file_from_path as file_from_path, strip_not_given as strip_not_given, - deepcopy_minimal as deepcopy_minimal, get_async_library as get_async_library, maybe_coerce_float as maybe_coerce_float, get_required_header as get_required_header, diff --git a/src/kernel/_utils/_utils.py b/src/kernel/_utils/_utils.py index 63b8cd60..771859f5 100644 --- a/src/kernel/_utils/_utils.py +++ b/src/kernel/_utils/_utils.py @@ -177,21 +177,6 @@ def is_iterable(obj: object) -> TypeGuard[Iterable[object]]: return isinstance(obj, Iterable) -def deepcopy_minimal(item: _T) -> _T: - """Minimal reimplementation of copy.deepcopy() that will only copy certain object types: - - - mappings, e.g. `dict` - - list - - This is done for performance reasons. - """ - if is_mapping(item): - return cast(_T, {k: deepcopy_minimal(v) for k, v in item.items()}) - if is_list(item): - return cast(_T, [deepcopy_minimal(entry) for entry in item]) - return item - - # copied from https://github.com/Rapptz/RoboDanny def human_join(seq: Sequence[str], *, delim: str = ", ", final: str = "or") -> str: size = len(seq) diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 3fc1049e..228e653a 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -49,8 +49,9 @@ ReplaysResourceWithStreamingResponse, AsyncReplaysResourceWithStreamingResponse, ) +from ..._files import deepcopy_with_paths from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given -from ..._utils import extract_files, path_template, maybe_transform, deepcopy_minimal, async_maybe_transform +from ..._utils import extract_files, path_template, maybe_transform, async_maybe_transform from .computer import ( ComputerResource, AsyncComputerResource, @@ -573,7 +574,7 @@ def load_extensions( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} - body = deepcopy_minimal({"extensions": extensions}) + body = deepcopy_with_paths({"extensions": extensions}, [["extensions", "", "zip_file"]]) files = extract_files(cast(Mapping[str, object], body), paths=[["extensions", "", "zip_file"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. @@ -1075,7 +1076,7 @@ async def load_extensions( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} - body = deepcopy_minimal({"extensions": extensions}) + body = deepcopy_with_paths({"extensions": extensions}, [["extensions", "", "zip_file"]]) files = extract_files(cast(Mapping[str, object], body), paths=[["extensions", "", "zip_file"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. diff --git a/src/kernel/resources/browsers/fs/fs.py b/src/kernel/resources/browsers/fs/fs.py index f26119fb..17149d51 100644 --- a/src/kernel/resources/browsers/fs/fs.py +++ b/src/kernel/resources/browsers/fs/fs.py @@ -15,7 +15,7 @@ WatchResourceWithStreamingResponse, AsyncWatchResourceWithStreamingResponse, ) -from ...._files import read_file_content, async_read_file_content +from ...._files import read_file_content, deepcopy_with_paths, async_read_file_content from ...._types import ( Body, Omit, @@ -30,7 +30,7 @@ omit, not_given, ) -from ...._utils import extract_files, path_template, maybe_transform, deepcopy_minimal, async_maybe_transform +from ...._utils import extract_files, path_template, maybe_transform, async_maybe_transform from ...._compat import cached_property from ...._resource import SyncAPIResource, AsyncAPIResource from ...._response import ( @@ -509,7 +509,7 @@ def upload( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} - body = deepcopy_minimal({"files": files}) + body = deepcopy_with_paths({"files": files}, [["files", "", "file"]]) extracted_files = extract_files(cast(Mapping[str, object], body), paths=[["files", "", "file"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. @@ -555,11 +555,12 @@ def upload_zip( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} - body = deepcopy_minimal( + body = deepcopy_with_paths( { "dest_path": dest_path, "zip_file": zip_file, - } + }, + [["zip_file"]], ) files = extract_files(cast(Mapping[str, object], body), paths=[["zip_file"]]) # It should be noted that the actual Content-Type header that will be @@ -1071,7 +1072,7 @@ async def upload( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} - body = deepcopy_minimal({"files": files}) + body = deepcopy_with_paths({"files": files}, [["files", "", "file"]]) extracted_files = extract_files(cast(Mapping[str, object], body), paths=[["files", "", "file"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. @@ -1117,11 +1118,12 @@ async def upload_zip( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} - body = deepcopy_minimal( + body = deepcopy_with_paths( { "dest_path": dest_path, "zip_file": zip_file, - } + }, + [["zip_file"]], ) files = extract_files(cast(Mapping[str, object], body), paths=[["zip_file"]]) # It should be noted that the actual Content-Type header that will be diff --git a/src/kernel/resources/deployments.py b/src/kernel/resources/deployments.py index 6b2c7601..9b079eb9 100644 --- a/src/kernel/resources/deployments.py +++ b/src/kernel/resources/deployments.py @@ -8,8 +8,9 @@ import httpx from ..types import deployment_list_params, deployment_create_params, deployment_follow_params +from .._files import deepcopy_with_paths from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, FileTypes, omit, not_given -from .._utils import extract_files, path_template, maybe_transform, deepcopy_minimal, async_maybe_transform +from .._utils import extract_files, path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -95,7 +96,7 @@ def create( timeout: Override the client-level default timeout for this request, in seconds """ - body = deepcopy_minimal( + body = deepcopy_with_paths( { "entrypoint_rel_path": entrypoint_rel_path, "env_vars": env_vars, @@ -104,7 +105,8 @@ def create( "region": region, "source": source, "version": version, - } + }, + [["file"]], ) files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be @@ -360,7 +362,7 @@ async def create( timeout: Override the client-level default timeout for this request, in seconds """ - body = deepcopy_minimal( + body = deepcopy_with_paths( { "entrypoint_rel_path": entrypoint_rel_path, "env_vars": env_vars, @@ -369,7 +371,8 @@ async def create( "region": region, "source": source, "version": version, - } + }, + [["file"]], ) files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be diff --git a/src/kernel/resources/extensions.py b/src/kernel/resources/extensions.py index c429b3c5..9ea6e0be 100644 --- a/src/kernel/resources/extensions.py +++ b/src/kernel/resources/extensions.py @@ -8,8 +8,9 @@ import httpx from ..types import extension_upload_params, extension_download_from_chrome_store_params +from .._files import deepcopy_with_paths from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, FileTypes, omit, not_given -from .._utils import extract_files, path_template, maybe_transform, deepcopy_minimal, async_maybe_transform +from .._utils import extract_files, path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -220,11 +221,12 @@ def upload( timeout: Override the client-level default timeout for this request, in seconds """ - body = deepcopy_minimal( + body = deepcopy_with_paths( { "file": file, "name": name, - } + }, + [["file"]], ) files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be @@ -429,11 +431,12 @@ async def upload( timeout: Override the client-level default timeout for this request, in seconds """ - body = deepcopy_minimal( + body = deepcopy_with_paths( { "file": file, "name": name, - } + }, + [["file"]], ) files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py deleted file mode 100644 index 83b72cd9..00000000 --- a/tests/test_deepcopy.py +++ /dev/null @@ -1,58 +0,0 @@ -from kernel._utils import deepcopy_minimal - - -def assert_different_identities(obj1: object, obj2: object) -> None: - assert obj1 == obj2 - assert id(obj1) != id(obj2) - - -def test_simple_dict() -> None: - obj1 = {"foo": "bar"} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - - -def test_nested_dict() -> None: - obj1 = {"foo": {"bar": True}} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert_different_identities(obj1["foo"], obj2["foo"]) - - -def test_complex_nested_dict() -> None: - obj1 = {"foo": {"bar": [{"hello": "world"}]}} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert_different_identities(obj1["foo"], obj2["foo"]) - assert_different_identities(obj1["foo"]["bar"], obj2["foo"]["bar"]) - assert_different_identities(obj1["foo"]["bar"][0], obj2["foo"]["bar"][0]) - - -def test_simple_list() -> None: - obj1 = ["a", "b", "c"] - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - - -def test_nested_list() -> None: - obj1 = ["a", [1, 2, 3]] - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert_different_identities(obj1[1], obj2[1]) - - -class MyObject: ... - - -def test_ignores_other_types() -> None: - # custom classes - my_obj = MyObject() - obj1 = {"foo": my_obj} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert obj1["foo"] is my_obj - - # tuples - obj3 = ("a", "b") - obj4 = deepcopy_minimal(obj3) - assert obj3 is obj4 diff --git a/tests/test_files.py b/tests/test_files.py index 62b874f0..078385df 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -4,7 +4,8 @@ import pytest from dirty_equals import IsDict, IsList, IsBytes, IsTuple -from kernel._files import to_httpx_files, async_to_httpx_files +from kernel._files import to_httpx_files, deepcopy_with_paths, async_to_httpx_files +from kernel._utils import extract_files readme_path = Path(__file__).parent.parent.joinpath("README.md") @@ -49,3 +50,99 @@ def test_string_not_allowed() -> None: "file": "foo", # type: ignore } ) + + +def assert_different_identities(obj1: object, obj2: object) -> None: + assert obj1 == obj2 + assert obj1 is not obj2 + + +class TestDeepcopyWithPaths: + def test_copies_top_level_dict(self) -> None: + original = {"file": b"data", "other": "value"} + result = deepcopy_with_paths(original, [["file"]]) + assert_different_identities(result, original) + + def test_file_value_is_same_reference(self) -> None: + file_bytes = b"contents" + original = {"file": file_bytes} + result = deepcopy_with_paths(original, [["file"]]) + assert_different_identities(result, original) + assert result["file"] is file_bytes + + def test_list_popped_wholesale(self) -> None: + files = [b"f1", b"f2"] + original = {"files": files, "title": "t"} + result = deepcopy_with_paths(original, [["files", ""]]) + assert_different_identities(result, original) + result_files = result["files"] + assert isinstance(result_files, list) + assert_different_identities(result_files, files) + + def test_nested_array_path_copies_list_and_elements(self) -> None: + elem1 = {"file": b"f1", "extra": 1} + elem2 = {"file": b"f2", "extra": 2} + original = {"items": [elem1, elem2]} + result = deepcopy_with_paths(original, [["items", "", "file"]]) + assert_different_identities(result, original) + result_items = result["items"] + assert isinstance(result_items, list) + assert_different_identities(result_items, original["items"]) + assert_different_identities(result_items[0], elem1) + assert_different_identities(result_items[1], elem2) + + def test_empty_paths_returns_same_object(self) -> None: + original = {"foo": "bar"} + result = deepcopy_with_paths(original, []) + assert result is original + + def test_multiple_paths(self) -> None: + f1 = b"file1" + f2 = b"file2" + original = {"a": f1, "b": f2, "c": "unchanged"} + result = deepcopy_with_paths(original, [["a"], ["b"]]) + assert_different_identities(result, original) + assert result["a"] is f1 + assert result["b"] is f2 + assert result["c"] is original["c"] + + def test_extract_files_does_not_mutate_original_top_level(self) -> None: + file_bytes = b"contents" + original = {"file": file_bytes, "other": "value"} + + copied = deepcopy_with_paths(original, [["file"]]) + extracted = extract_files(copied, paths=[["file"]]) + + assert extracted == [("file", file_bytes)] + assert original == {"file": file_bytes, "other": "value"} + assert copied == {"other": "value"} + + def test_extract_files_does_not_mutate_original_nested_array_path(self) -> None: + file1 = b"f1" + file2 = b"f2" + original = { + "items": [ + {"file": file1, "extra": 1}, + {"file": file2, "extra": 2}, + ], + "title": "example", + } + + copied = deepcopy_with_paths(original, [["items", "", "file"]]) + extracted = extract_files(copied, paths=[["items", "", "file"]]) + + assert extracted == [("items[][file]", file1), ("items[][file]", file2)] + assert original == { + "items": [ + {"file": file1, "extra": 1}, + {"file": file2, "extra": 2}, + ], + "title": "example", + } + assert copied == { + "items": [ + {"extra": 1}, + {"extra": 2}, + ], + "title": "example", + } From 1079e3ae7520e79150321d643cee6436179bbda0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 03:54:01 +0000 Subject: [PATCH 368/448] fix: include MFA and sign-in options in CUA SSO-only step response --- .stats.yml | 4 ++-- src/kernel/types/auth/connection_follow_response.py | 6 ++++++ src/kernel/types/auth/managed_auth.py | 6 ++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index f0ba92fd..a0c2ee4b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-930823e8b25b4644b74098ad5479840f64a329321aa236460f8a9562ae9051bf.yml -openapi_spec_hash: 9f868e67df8fd2fec8d8fc3eb5ba0b26 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-57bfd737f612a4f16965ef7eb85f709752f616a3941c451736a9ad76e4a1135f.yml +openapi_spec_hash: 61d13a607970deefff0cbfe6b77ae6e8 config_hash: 08d55086449943a8fec212b870061a3f diff --git a/src/kernel/types/auth/connection_follow_response.py b/src/kernel/types/auth/connection_follow_response.py index 4eeba5c5..a9bda35d 100644 --- a/src/kernel/types/auth/connection_follow_response.py +++ b/src/kernel/types/auth/connection_follow_response.py @@ -34,6 +34,12 @@ class ManagedAuthStateEventDiscoveredField(BaseModel): type: Literal["text", "email", "password", "tel", "number", "url", "code", "totp"] """Field type""" + hint: Optional[str] = None + """ + Contextual help text near the field that tells the user what to enter (e.g., + "Enter the phone ending in (**_) _**-\\**\\**92") + """ + linked_mfa_type: Optional[Literal["sms", "call", "email", "totp", "push", "password"]] = None """ If this field is associated with an MFA option, the type of that option (e.g., diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py index 10dea637..d76a02f4 100644 --- a/src/kernel/types/auth/managed_auth.py +++ b/src/kernel/types/auth/managed_auth.py @@ -46,6 +46,12 @@ class DiscoveredField(BaseModel): type: Literal["text", "email", "password", "tel", "number", "url", "code", "totp"] """Field type""" + hint: Optional[str] = None + """ + Contextual help text near the field that tells the user what to enter (e.g., + "Enter the phone ending in (**_) _**-\\**\\**92") + """ + linked_mfa_type: Optional[Literal["sms", "call", "email", "totp", "push", "password"]] = None """ If this field is associated with an MFA option, the type of that option (e.g., From 7d44c3e5cee9071cedef3f909a9ec3383ef2395f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:08:49 +0000 Subject: [PATCH 369/448] feat: remove paid plan gating from project endpoints --- .stats.yml | 4 ++-- src/kernel/resources/projects/projects.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.stats.yml b/.stats.yml index a0c2ee4b..bed10345 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-57bfd737f612a4f16965ef7eb85f709752f616a3941c451736a9ad76e4a1135f.yml -openapi_spec_hash: 61d13a607970deefff0cbfe6b77ae6e8 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-686a9addd4f9356ca26ff3ff04e1a11466d77a412859829075566394922b715d.yml +openapi_spec_hash: 7a9e9c2023400d44bcbfb87b7ec07708 config_hash: 08d55086449943a8fec212b870061a3f diff --git a/src/kernel/resources/projects/projects.py b/src/kernel/resources/projects/projects.py index b40dc02f..9a7f13b2 100644 --- a/src/kernel/resources/projects/projects.py +++ b/src/kernel/resources/projects/projects.py @@ -72,8 +72,8 @@ def create( ) -> Project: """Create a new project within the authenticated organization. - Requires a paid plan - and the projects feature flag. + Requires the + projects feature flag. Args: name: Project name (1-255 characters) @@ -297,8 +297,8 @@ async def create( ) -> Project: """Create a new project within the authenticated organization. - Requires a paid plan - and the projects feature flag. + Requires the + projects feature flag. Args: name: Project name (1-255 characters) From 3329c506d6a109917f040328a5bd2d53287ef568 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:38:38 +0000 Subject: [PATCH 370/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index ff661205..26b1ce24 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.48.0" + ".": "0.50.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index b26c2b0c..55169264 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.48.0" +version = "0.50.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index a9881de4..81d0216c 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.48.0" # x-release-please-version +__version__ = "0.50.0" # x-release-please-version From c0230197eb11e61acb042bc956198c0067b88bb3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 03:08:40 +0000 Subject: [PATCH 371/448] chore(internal): more robust bootstrap script --- scripts/bootstrap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/bootstrap b/scripts/bootstrap index b430fee3..fe8451e4 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -4,7 +4,7 @@ set -e cd "$(dirname "$0")/.." -if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then +if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "${SKIP_BREW:-}" != "1" ] && [ -t 0 ]; then brew bundle check >/dev/null 2>&1 || { echo -n "==> Install Homebrew dependencies? (y/N): " read -r response From 51d5a08be591e50b9c4de7cbb8c726ec429f5f00 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 17:57:01 +0000 Subject: [PATCH 372/448] feat: Expose browser_session_id on managed auth connection --- .stats.yml | 4 ++-- src/kernel/types/auth/managed_auth.py | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index bed10345..fec95fa1 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-686a9addd4f9356ca26ff3ff04e1a11466d77a412859829075566394922b715d.yml -openapi_spec_hash: 7a9e9c2023400d44bcbfb87b7ec07708 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-e14974fd90680e5745b35d8718a1ccce2181f6d17a6e0a1fd35fc5bca88795ae.yml +openapi_spec_hash: 1b3aa75f0ab48b122d514047f9c82873 config_hash: 08d55086449943a8fec212b870061a3f diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py index d76a02f4..0ac541a2 100644 --- a/src/kernel/types/auth/managed_auth.py +++ b/src/kernel/types/auth/managed_auth.py @@ -157,6 +157,13 @@ class ManagedAuth(BaseModel): - Ping Identity: _.pingone.com, _.pingidentity.com """ + browser_session_id: Optional[str] = None + """ + ID of the underlying browser session driving the current flow (present when flow + in progress). Use this to inspect or terminate the browser session via the + `/browsers` API. + """ + can_reauth: Optional[bool] = None """ Whether automatic re-authentication is possible (has credential, selectors, and From 78c801d80886d37ce9da5ff2d08b5a4dac571bb7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 25 Apr 2026 16:29:18 +0000 Subject: [PATCH 373/448] feat: Expire stuck IN_PROGRESS managed auth sessions via background worker --- .stats.yml | 4 ++-- src/kernel/types/auth/managed_auth.py | 22 ++++++++++++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/.stats.yml b/.stats.yml index fec95fa1..d58220c1 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-e14974fd90680e5745b35d8718a1ccce2181f6d17a6e0a1fd35fc5bca88795ae.yml -openapi_spec_hash: 1b3aa75f0ab48b122d514047f9c82873 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-a674e3c4c0063942621d1b4e7f67b72f7e240c12dd88564fe16627618ba33dd6.yml +openapi_spec_hash: 8b97c87f0dafe5fc5e5a7365f3687755 config_hash: 08d55086449943a8fec212b870061a3f diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py index 0ac541a2..59b2d576 100644 --- a/src/kernel/types/auth/managed_auth.py +++ b/src/kernel/types/auth/managed_auth.py @@ -197,7 +197,12 @@ class ManagedAuth(BaseModel): """ flow_expires_at: Optional[datetime] = None - """When the current flow expires (null when no flow in progress)""" + """When the current flow expires (null when no flow in progress). + + A flow past this timestamp is no longer valid and its `flow_status` will be + `EXPIRED`. Clients may start a new login to supersede a stale `IN_PROGRESS` flow + past this timestamp. + """ flow_status: Optional[Literal["IN_PROGRESS", "SUCCESS", "FAILED", "EXPIRED", "CANCELED"]] = None """Current flow status (null when no flow in progress)""" @@ -223,7 +228,20 @@ class ManagedAuth(BaseModel): """URL to redirect user to for hosted login (present when flow in progress)""" last_auth_at: Optional[datetime] = None - """When the profile was last successfully authenticated""" + """Deprecated alias for `last_auth_check_at`. + + Despite the name, this is the last health-check timestamp, not the last + successful authentication. Use `last_auth_check_at` instead. + """ + + last_auth_check_at: Optional[datetime] = None + """ + When the most recent auth health check ran for this connection, regardless of + outcome. Updated on every health check and does not by itself indicate that the + profile is currently authenticated - use `status` for that. May be newer than + `flow_expires_at` when a flow is still in progress because health checks + continue to run in parallel. + """ live_view_url: Optional[str] = None """Browser live view URL for debugging (present when flow in progress)""" From a260675263c9f8377200712c2840e59101ca8846 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 19:54:47 +0000 Subject: [PATCH 374/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 26b1ce24..2b2b4fa9 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.50.0" + ".": "0.51.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 55169264..6ff6d3c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.50.0" +version = "0.51.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 81d0216c..e0b52d16 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.50.0" # x-release-please-version +__version__ = "0.51.0" # x-release-please-version From 497e1ce9561ce420150af6b13f6391ec3808d018 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 03:03:01 +0000 Subject: [PATCH 375/448] fix: use correct field name format for multipart file arrays --- src/kernel/_qs.py | 8 ++----- src/kernel/_types.py | 3 +++ src/kernel/_utils/_utils.py | 42 ++++++++++++++++++++++++++++++------- tests/test_extract_files.py | 28 ++++++++++++++++++++----- tests/test_files.py | 2 +- 5 files changed, 63 insertions(+), 20 deletions(-) diff --git a/src/kernel/_qs.py b/src/kernel/_qs.py index de8c99bc..4127c19c 100644 --- a/src/kernel/_qs.py +++ b/src/kernel/_qs.py @@ -2,17 +2,13 @@ from typing import Any, List, Tuple, Union, Mapping, TypeVar from urllib.parse import parse_qs, urlencode -from typing_extensions import Literal, get_args +from typing_extensions import get_args -from ._types import NotGiven, not_given +from ._types import NotGiven, ArrayFormat, NestedFormat, not_given from ._utils import flatten _T = TypeVar("_T") - -ArrayFormat = Literal["comma", "repeat", "indices", "brackets"] -NestedFormat = Literal["dots", "brackets"] - PrimitiveData = Union[str, int, float, bool, None] # this should be Data = Union[PrimitiveData, "List[Data]", "Tuple[Data]", "Mapping[str, Data]"] # https://github.com/microsoft/pyright/issues/3555 diff --git a/src/kernel/_types.py b/src/kernel/_types.py index 28254f95..d924c204 100644 --- a/src/kernel/_types.py +++ b/src/kernel/_types.py @@ -47,6 +47,9 @@ ModelT = TypeVar("ModelT", bound=pydantic.BaseModel) _T = TypeVar("_T") +ArrayFormat = Literal["comma", "repeat", "indices", "brackets"] +NestedFormat = Literal["dots", "brackets"] + # Approximates httpx internal ProxiesTypes and RequestFiles types # while adding support for `PathLike` instances diff --git a/src/kernel/_utils/_utils.py b/src/kernel/_utils/_utils.py index 771859f5..199cd231 100644 --- a/src/kernel/_utils/_utils.py +++ b/src/kernel/_utils/_utils.py @@ -17,11 +17,11 @@ ) from pathlib import Path from datetime import date, datetime -from typing_extensions import TypeGuard +from typing_extensions import TypeGuard, get_args import sniffio -from .._types import Omit, NotGiven, FileTypes, HeadersLike +from .._types import Omit, NotGiven, FileTypes, ArrayFormat, HeadersLike _T = TypeVar("_T") _TupleT = TypeVar("_TupleT", bound=Tuple[object, ...]) @@ -40,25 +40,45 @@ def extract_files( query: Mapping[str, object], *, paths: Sequence[Sequence[str]], + array_format: ArrayFormat = "brackets", ) -> list[tuple[str, FileTypes]]: """Recursively extract files from the given dictionary based on specified paths. A path may look like this ['foo', 'files', '', 'data']. + ``array_format`` controls how ```` segments contribute to the emitted + field name. Supported values: ``"brackets"`` (``foo[]``), ``"repeat"`` and + ``"comma"`` (``foo``), ``"indices"`` (``foo[0]``, ``foo[1]``). + Note: this mutates the given dictionary. """ files: list[tuple[str, FileTypes]] = [] for path in paths: - files.extend(_extract_items(query, path, index=0, flattened_key=None)) + files.extend(_extract_items(query, path, index=0, flattened_key=None, array_format=array_format)) return files +def _array_suffix(array_format: ArrayFormat, array_index: int) -> str: + if array_format == "brackets": + return "[]" + if array_format == "indices": + return f"[{array_index}]" + if array_format == "repeat" or array_format == "comma": + # Both repeat the bare field name for each file part; there is no + # meaningful way to comma-join binary parts. + return "" + raise NotImplementedError( + f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}" + ) + + def _extract_items( obj: object, path: Sequence[str], *, index: int, flattened_key: str | None, + array_format: ArrayFormat, ) -> list[tuple[str, FileTypes]]: try: key = path[index] @@ -75,9 +95,11 @@ def _extract_items( if is_list(obj): files: list[tuple[str, FileTypes]] = [] - for entry in obj: - assert_is_file_content(entry, key=flattened_key + "[]" if flattened_key else "") - files.append((flattened_key + "[]", cast(FileTypes, entry))) + for array_index, entry in enumerate(obj): + suffix = _array_suffix(array_format, array_index) + emitted_key = (flattened_key + suffix) if flattened_key else suffix + assert_is_file_content(entry, key=emitted_key) + files.append((emitted_key, cast(FileTypes, entry))) return files assert_is_file_content(obj, key=flattened_key) @@ -106,6 +128,7 @@ def _extract_items( path, index=index, flattened_key=flattened_key, + array_format=array_format, ) elif is_list(obj): if key != "": @@ -117,9 +140,12 @@ def _extract_items( item, path, index=index, - flattened_key=flattened_key + "[]" if flattened_key is not None else "[]", + flattened_key=( + (flattened_key if flattened_key is not None else "") + _array_suffix(array_format, array_index) + ), + array_format=array_format, ) - for item in obj + for array_index, item in enumerate(obj) ] ) diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py index 14a3932a..54ef03af 100644 --- a/tests/test_extract_files.py +++ b/tests/test_extract_files.py @@ -4,7 +4,7 @@ import pytest -from kernel._types import FileTypes +from kernel._types import FileTypes, ArrayFormat from kernel._utils import extract_files @@ -37,10 +37,7 @@ def test_multiple_files() -> None: def test_top_level_file_array() -> None: query = {"files": [b"file one", b"file two"], "title": "hello"} - assert extract_files(query, paths=[["files", ""]]) == [ - ("files[]", b"file one"), - ("files[]", b"file two"), - ] + assert extract_files(query, paths=[["files", ""]]) == [("files[]", b"file one"), ("files[]", b"file two")] assert query == {"title": "hello"} @@ -71,3 +68,24 @@ def test_ignores_incorrect_paths( expected: list[tuple[str, FileTypes]], ) -> None: assert extract_files(query, paths=paths) == expected + + +@pytest.mark.parametrize( + "array_format,expected_top_level,expected_nested", + [ + ("brackets", [("files[]", b"a"), ("files[]", b"b")], [("items[][file]", b"a"), ("items[][file]", b"b")]), + ("repeat", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]), + ("comma", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]), + ("indices", [("files[0]", b"a"), ("files[1]", b"b")], [("items[0][file]", b"a"), ("items[1][file]", b"b")]), + ], +) +def test_array_format_controls_file_field_names( + array_format: ArrayFormat, + expected_top_level: list[tuple[str, FileTypes]], + expected_nested: list[tuple[str, FileTypes]], +) -> None: + top_level = {"files": [b"a", b"b"]} + assert extract_files(top_level, paths=[["files", ""]], array_format=array_format) == expected_top_level + + nested = {"items": [{"file": b"a"}, {"file": b"b"}]} + assert extract_files(nested, paths=[["items", "", "file"]], array_format=array_format) == expected_nested diff --git a/tests/test_files.py b/tests/test_files.py index 078385df..2bbbef20 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -131,7 +131,7 @@ def test_extract_files_does_not_mutate_original_nested_array_path(self) -> None: copied = deepcopy_with_paths(original, [["items", "", "file"]]) extracted = extract_files(copied, paths=[["items", "", "file"]]) - assert extracted == [("items[][file]", file1), ("items[][file]", file2)] + assert [entry for _, entry in extracted] == [file1, file2] assert original == { "items": [ {"file": file1, "extra": 1}, From 976d0d76c7e1084b78ff563b9078dea4dda6e96d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 03:04:06 +0000 Subject: [PATCH 376/448] feat: support setting headers via env --- src/kernel/_client.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/kernel/_client.py b/src/kernel/_client.py index 75fe4b64..c2c7f4b8 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -19,7 +19,11 @@ RequestOptions, not_given, ) -from ._utils import is_given, get_async_library +from ._utils import ( + is_given, + is_mapping_t, + get_async_library, +) from ._compat import cached_property from ._version import __version__ from ._streaming import Stream as Stream, AsyncStream as AsyncStream @@ -144,6 +148,15 @@ def __init__( except KeyError as exc: raise ValueError(f"Unknown environment: {environment}") from exc + custom_headers_env = os.environ.get("KERNEL_CUSTOM_HEADERS") + if custom_headers_env is not None: + parsed: dict[str, str] = {} + for line in custom_headers_env.split("\n"): + colon = line.find(":") + if colon >= 0: + parsed[line[:colon].strip()] = line[colon + 1 :].strip() + default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})} + super().__init__( version=__version__, base_url=base_url, @@ -421,6 +434,15 @@ def __init__( except KeyError as exc: raise ValueError(f"Unknown environment: {environment}") from exc + custom_headers_env = os.environ.get("KERNEL_CUSTOM_HEADERS") + if custom_headers_env is not None: + parsed: dict[str, str] = {} + for line in custom_headers_env.split("\n"): + colon = line.find(":") + if colon >= 0: + parsed[line[:colon].strip()] = line[colon + 1 :].strip() + default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})} + super().__init__( version=__version__, base_url=base_url, From 018a28e606840222fc3fd43fa856fa2bca34ec01 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:36:36 +0000 Subject: [PATCH 377/448] feat: profile download: 409 for empty profile + surface API errors in dashboard --- .stats.yml | 4 ++-- src/kernel/resources/profiles.py | 12 ++++-------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/.stats.yml b/.stats.yml index d58220c1..33f284fe 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-a674e3c4c0063942621d1b4e7f67b72f7e240c12dd88564fe16627618ba33dd6.yml -openapi_spec_hash: 8b97c87f0dafe5fc5e5a7365f3687755 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-4ce09d1a7546ab36f578cb27d819187eeb90c580b11834c7ff7a375aa22f9a20.yml +openapi_spec_hash: 1043ab2d699f6c828680c3352cd4cece config_hash: 08d55086449943a8fec212b870061a3f diff --git a/src/kernel/resources/profiles.py b/src/kernel/resources/profiles.py index ec3d3fcc..c20ffb7d 100644 --- a/src/kernel/resources/profiles.py +++ b/src/kernel/resources/profiles.py @@ -216,10 +216,8 @@ def download( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> BinaryAPIResponse: - """Download the profile. - - Profiles are JSON files containing the pieces of state - that we save. + """ + Returns a zstd-compressed tar file of the full user-data directory. Args: extra_headers: Send extra headers @@ -428,10 +426,8 @@ async def download( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncBinaryAPIResponse: - """Download the profile. - - Profiles are JSON files containing the pieces of state - that we save. + """ + Returns a zstd-compressed tar file of the full user-data directory. Args: extra_headers: Send extra headers From 8f9d0dfc9fe008e4b6babef30eddf4d09f3ac33e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:35:49 +0000 Subject: [PATCH 378/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 2b2b4fa9..fed4b17f 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.51.0" + ".": "0.52.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 6ff6d3c4..13b5e4ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.51.0" +version = "0.52.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index e0b52d16..1ce5f4bd 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.51.0" # x-release-please-version +__version__ = "0.52.0" # x-release-please-version From 066d6916999afc46f089f5625cc7cc9a4917109d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 07:27:20 +0000 Subject: [PATCH 379/448] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 33f284fe..324dd3f5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-4ce09d1a7546ab36f578cb27d819187eeb90c580b11834c7ff7a375aa22f9a20.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-4ce09d1a7546ab36f578cb27d819187eeb90c580b11834c7ff7a375aa22f9a20.yml openapi_spec_hash: 1043ab2d699f6c828680c3352cd4cece config_hash: 08d55086449943a8fec212b870061a3f From 46683f853ebcf64b8507ebe003e145428a6902f9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 14:55:39 +0000 Subject: [PATCH 380/448] feat: Add 'switch' MFA option type for generic method-switcher links --- .stats.yml | 4 ++-- src/kernel/types/auth/connection_follow_response.py | 11 +++++++---- src/kernel/types/auth/managed_auth.py | 11 +++++++---- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/.stats.yml b/.stats.yml index 324dd3f5..d0f4a0f3 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-4ce09d1a7546ab36f578cb27d819187eeb90c580b11834c7ff7a375aa22f9a20.yml -openapi_spec_hash: 1043ab2d699f6c828680c3352cd4cece +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-81659c4d18e7992d17a0930d6c13c8592a0ff5bb974ea9e2e4b3f46d43b117d2.yml +openapi_spec_hash: f3d12a3a0a5e9ce711fb1c571ee36f9c config_hash: 08d55086449943a8fec212b870061a3f diff --git a/src/kernel/types/auth/connection_follow_response.py b/src/kernel/types/auth/connection_follow_response.py index a9bda35d..7cabc1c8 100644 --- a/src/kernel/types/auth/connection_follow_response.py +++ b/src/kernel/types/auth/connection_follow_response.py @@ -40,7 +40,7 @@ class ManagedAuthStateEventDiscoveredField(BaseModel): "Enter the phone ending in (**_) _**-\\**\\**92") """ - linked_mfa_type: Optional[Literal["sms", "call", "email", "totp", "push", "password"]] = None + linked_mfa_type: Optional[Literal["sms", "call", "email", "totp", "push", "password", "switch"]] = None """ If this field is associated with an MFA option, the type of that option (e.g., password field linked to "Enter password" option) @@ -59,9 +59,12 @@ class ManagedAuthStateEventMfaOption(BaseModel): label: str """The visible option text""" - type: Literal["sms", "call", "email", "totp", "push", "password"] - """ - The MFA delivery method type (includes password for auth method selection pages) + type: Literal["sms", "call", "email", "totp", "push", "password", "switch"] + """The MFA delivery method type. + + Includes 'password' for auth method selection pages and 'switch' for generic + method-switcher links like "Use another method" that do not name a specific + method. """ description: Optional[str] = None diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py index 59b2d576..622c3bd5 100644 --- a/src/kernel/types/auth/managed_auth.py +++ b/src/kernel/types/auth/managed_auth.py @@ -52,7 +52,7 @@ class DiscoveredField(BaseModel): "Enter the phone ending in (**_) _**-\\**\\**92") """ - linked_mfa_type: Optional[Literal["sms", "call", "email", "totp", "push", "password"]] = None + linked_mfa_type: Optional[Literal["sms", "call", "email", "totp", "push", "password", "switch"]] = None """ If this field is associated with an MFA option, the type of that option (e.g., password field linked to "Enter password" option) @@ -71,9 +71,12 @@ class MfaOption(BaseModel): label: str """The visible option text""" - type: Literal["sms", "call", "email", "totp", "push", "password"] - """ - The MFA delivery method type (includes password for auth method selection pages) + type: Literal["sms", "call", "email", "totp", "push", "password", "switch"] + """The MFA delivery method type. + + Includes 'password' for auth method selection pages and 'switch' for generic + method-switcher links like "Use another method" that do not name a specific + method. """ description: Optional[str] = None From 9054b0828167c06c054b4e6179055e2013c5c51b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 03:39:52 +0000 Subject: [PATCH 381/448] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index d0f4a0f3..d2492a7d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-81659c4d18e7992d17a0930d6c13c8592a0ff5bb974ea9e2e4b3f46d43b117d2.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-718b49461ceaa1d6cac7854c29dfef9036a83f6632e756c9d1ecf31fd77c57f6.yml openapi_spec_hash: f3d12a3a0a5e9ce711fb1c571ee36f9c config_hash: 08d55086449943a8fec212b870061a3f From 12afbecb908b1b34b86c26dcd86255ead7cd10ac Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 03:43:46 +0000 Subject: [PATCH 382/448] chore(internal): reformat pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 13b5e4ca..5d43220b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -168,7 +168,7 @@ show_error_codes = true # # We also exclude our `tests` as mypy doesn't always infer # types correctly and Pyright will still catch any type errors. -exclude = ['src/kernel/_files.py', '_dev/.*.py', 'tests/.*'] +exclude = ["src/kernel/_files.py", "_dev/.*.py", "tests/.*"] strict_equality = true implicit_reexport = true From a908ad13bddfdd545859ffe03e44cada559e1303 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 2 May 2026 16:48:46 +0000 Subject: [PATCH 383/448] feat(api): server-side search on GET /projects --- .stats.yml | 4 ++-- src/kernel/resources/projects/projects.py | 8 ++++++++ src/kernel/types/project_list_params.py | 3 +++ tests/api_resources/test_projects.py | 2 ++ 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index d2492a7d..229ced73 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-718b49461ceaa1d6cac7854c29dfef9036a83f6632e756c9d1ecf31fd77c57f6.yml -openapi_spec_hash: f3d12a3a0a5e9ce711fb1c571ee36f9c +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-86e061884d273a27064593a0de3a4ba366a12a1001181741addb4f781be18ec9.yml +openapi_spec_hash: e0a4ddf4a3302599376756127099488c config_hash: 08d55086449943a8fec212b870061a3f diff --git a/src/kernel/resources/projects/projects.py b/src/kernel/resources/projects/projects.py index 9a7f13b2..f73e70d7 100644 --- a/src/kernel/resources/projects/projects.py +++ b/src/kernel/resources/projects/projects.py @@ -179,6 +179,7 @@ def list( *, limit: int | Omit = omit, offset: int | Omit = omit, + query: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -194,6 +195,8 @@ def list( offset: Number of results to skip + query: Case-insensitive substring match against project name + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -214,6 +217,7 @@ def list( { "limit": limit, "offset": offset, + "query": query, }, project_list_params.ProjectListParams, ), @@ -404,6 +408,7 @@ def list( *, limit: int | Omit = omit, offset: int | Omit = omit, + query: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -419,6 +424,8 @@ def list( offset: Number of results to skip + query: Case-insensitive substring match against project name + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -439,6 +446,7 @@ def list( { "limit": limit, "offset": offset, + "query": query, }, project_list_params.ProjectListParams, ), diff --git a/src/kernel/types/project_list_params.py b/src/kernel/types/project_list_params.py index ea10f073..9e740e60 100644 --- a/src/kernel/types/project_list_params.py +++ b/src/kernel/types/project_list_params.py @@ -13,3 +13,6 @@ class ProjectListParams(TypedDict, total=False): offset: int """Number of results to skip""" + + query: str + """Case-insensitive substring match against project name""" diff --git a/tests/api_resources/test_projects.py b/tests/api_resources/test_projects.py index 488c191d..bb301454 100644 --- a/tests/api_resources/test_projects.py +++ b/tests/api_resources/test_projects.py @@ -158,6 +158,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None: project = client.projects.list( limit=100, offset=0, + query="query", ) assert_matches_type(SyncOffsetPagination[Project], project, path=["response"]) @@ -371,6 +372,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N project = await async_client.projects.list( limit=100, offset=0, + query="query", ) assert_matches_type(AsyncOffsetPagination[Project], project, path=["response"]) From 8f6d6f9ab22e7f99fe29a6c13ce4d32210ab3176 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 21:20:12 +0000 Subject: [PATCH 384/448] feat: Scope name uniqueness to project for profiles, session_pools, extensions, credentials --- .stats.yml | 4 ++-- src/kernel/resources/browser_pools.py | 8 ++++---- src/kernel/resources/credentials.py | 4 ++-- src/kernel/resources/extensions.py | 4 ++-- src/kernel/resources/profiles.py | 4 ++-- src/kernel/types/browser_pool.py | 2 +- src/kernel/types/browser_pool_create_params.py | 2 +- src/kernel/types/browser_pool_update_params.py | 2 +- src/kernel/types/credential.py | 2 +- src/kernel/types/credential_create_params.py | 2 +- src/kernel/types/extension_list_response.py | 2 +- src/kernel/types/extension_upload_params.py | 2 +- src/kernel/types/extension_upload_response.py | 2 +- src/kernel/types/profile_create_params.py | 2 +- 14 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.stats.yml b/.stats.yml index 229ced73..797624b4 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-86e061884d273a27064593a0de3a4ba366a12a1001181741addb4f781be18ec9.yml -openapi_spec_hash: e0a4ddf4a3302599376756127099488c +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-b51c72a040c8dfea9c0693de6631feabfffe42922d5feb60b4ac0ee5c83bb8f4.yml +openapi_spec_hash: 8b671cfe4debe8d9ad7c39ba5b0eaf6d config_hash: 08d55086449943a8fec212b870061a3f diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py index 045737af..6c265cd6 100644 --- a/src/kernel/resources/browser_pools.py +++ b/src/kernel/resources/browser_pools.py @@ -100,7 +100,7 @@ def create( kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live view. - name: Optional name for the browser pool. Must be unique within the organization. + name: Optional name for the browser pool. Must be unique within the project. profile: Profile selection for the browser session. Provide either id or name. If specified, the matching profile will be loaded into the browser session. @@ -243,7 +243,7 @@ def update( kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live view. - name: Optional name for the browser pool. Must be unique within the organization. + name: Optional name for the browser pool. Must be unique within the project. profile: Profile selection for the browser session. Provide either id or name. If specified, the matching profile will be loaded into the browser session. @@ -562,7 +562,7 @@ async def create( kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live view. - name: Optional name for the browser pool. Must be unique within the organization. + name: Optional name for the browser pool. Must be unique within the project. profile: Profile selection for the browser session. Provide either id or name. If specified, the matching profile will be loaded into the browser session. @@ -705,7 +705,7 @@ async def update( kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live view. - name: Optional name for the browser pool. Must be unique within the organization. + name: Optional name for the browser pool. Must be unique within the project. profile: Profile selection for the browser session. Provide either id or name. If specified, the matching profile will be loaded into the browser session. diff --git a/src/kernel/resources/credentials.py b/src/kernel/resources/credentials.py index 093fcc5d..7d00faab 100644 --- a/src/kernel/resources/credentials.py +++ b/src/kernel/resources/credentials.py @@ -68,7 +68,7 @@ def create( Args: domain: Target domain this credential is for - name: Unique name for the credential within the organization + name: Unique name for the credential within the project values: Field name to value mapping (e.g., username, password) @@ -365,7 +365,7 @@ async def create( Args: domain: Target domain this credential is for - name: Unique name for the credential within the organization + name: Unique name for the credential within the project values: Field name to value mapping (e.g., username, password) diff --git a/src/kernel/resources/extensions.py b/src/kernel/resources/extensions.py index 9ea6e0be..d5cf0eb7 100644 --- a/src/kernel/resources/extensions.py +++ b/src/kernel/resources/extensions.py @@ -211,7 +211,7 @@ def upload( Args: file: ZIP file containing the browser extension. - name: Optional unique name within the organization to reference this extension. + name: Optional unique name within the project to reference this extension. extra_headers: Send extra headers @@ -421,7 +421,7 @@ async def upload( Args: file: ZIP file containing the browser extension. - name: Optional unique name within the organization to reference this extension. + name: Optional unique name within the project to reference this extension. extra_headers: Send extra headers diff --git a/src/kernel/resources/profiles.py b/src/kernel/resources/profiles.py index c20ffb7d..4083f12d 100644 --- a/src/kernel/resources/profiles.py +++ b/src/kernel/resources/profiles.py @@ -68,7 +68,7 @@ def create( sessions. Args: - name: Optional name of the profile. Must be unique within the organization. + name: Optional name of the profile. Must be unique within the project. extra_headers: Send extra headers @@ -278,7 +278,7 @@ async def create( sessions. Args: - name: Optional name of the profile. Must be unique within the organization. + name: Optional name of the profile. Must be unique within the project. extra_headers: Send extra headers diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py index 8ca0dc43..807c73b6 100644 --- a/src/kernel/types/browser_pool.py +++ b/src/kernel/types/browser_pool.py @@ -48,7 +48,7 @@ class BrowserPoolConfig(BaseModel): """ name: Optional[str] = None - """Optional name for the browser pool. Must be unique within the organization.""" + """Optional name for the browser pool. Must be unique within the project.""" profile: Optional[BrowserProfile] = None """Profile selection for the browser session. diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py index ecfb8881..cab4e138 100644 --- a/src/kernel/types/browser_pool_create_params.py +++ b/src/kernel/types/browser_pool_create_params.py @@ -47,7 +47,7 @@ class BrowserPoolCreateParams(TypedDict, total=False): """ name: str - """Optional name for the browser pool. Must be unique within the organization.""" + """Optional name for the browser pool. Must be unique within the project.""" profile: BrowserProfile """Profile selection for the browser session. diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py index e34664a4..7ddd1310 100644 --- a/src/kernel/types/browser_pool_update_params.py +++ b/src/kernel/types/browser_pool_update_params.py @@ -53,7 +53,7 @@ class BrowserPoolUpdateParams(TypedDict, total=False): """ name: str - """Optional name for the browser pool. Must be unique within the organization.""" + """Optional name for the browser pool. Must be unique within the project.""" profile: BrowserProfile """Profile selection for the browser session. diff --git a/src/kernel/types/credential.py b/src/kernel/types/credential.py index bbf2af5e..323617c5 100644 --- a/src/kernel/types/credential.py +++ b/src/kernel/types/credential.py @@ -21,7 +21,7 @@ class Credential(BaseModel): """Target domain this credential is for""" name: str - """Unique name for the credential within the organization""" + """Unique name for the credential within the project""" updated_at: datetime """When the credential was last updated""" diff --git a/src/kernel/types/credential_create_params.py b/src/kernel/types/credential_create_params.py index 94964b9d..9d306844 100644 --- a/src/kernel/types/credential_create_params.py +++ b/src/kernel/types/credential_create_params.py @@ -13,7 +13,7 @@ class CredentialCreateParams(TypedDict, total=False): """Target domain this credential is for""" name: Required[str] - """Unique name for the credential within the organization""" + """Unique name for the credential within the project""" values: Required[Dict[str, str]] """Field name to value mapping (e.g., username, password)""" diff --git a/src/kernel/types/extension_list_response.py b/src/kernel/types/extension_list_response.py index 79a5c991..bf9e544d 100644 --- a/src/kernel/types/extension_list_response.py +++ b/src/kernel/types/extension_list_response.py @@ -27,7 +27,7 @@ class ExtensionListResponseItem(BaseModel): name: Optional[str] = None """Optional, easier-to-reference name for the extension. - Must be unique within the organization. + Must be unique within the project. """ diff --git a/src/kernel/types/extension_upload_params.py b/src/kernel/types/extension_upload_params.py index d36dde31..ab44d3eb 100644 --- a/src/kernel/types/extension_upload_params.py +++ b/src/kernel/types/extension_upload_params.py @@ -14,4 +14,4 @@ class ExtensionUploadParams(TypedDict, total=False): """ZIP file containing the browser extension.""" name: str - """Optional unique name within the organization to reference this extension.""" + """Optional unique name within the project to reference this extension.""" diff --git a/src/kernel/types/extension_upload_response.py b/src/kernel/types/extension_upload_response.py index 1b3be221..068b25dd 100644 --- a/src/kernel/types/extension_upload_response.py +++ b/src/kernel/types/extension_upload_response.py @@ -26,5 +26,5 @@ class ExtensionUploadResponse(BaseModel): name: Optional[str] = None """Optional, easier-to-reference name for the extension. - Must be unique within the organization. + Must be unique within the project. """ diff --git a/src/kernel/types/profile_create_params.py b/src/kernel/types/profile_create_params.py index 0b2b12ae..bca1e17d 100644 --- a/src/kernel/types/profile_create_params.py +++ b/src/kernel/types/profile_create_params.py @@ -9,4 +9,4 @@ class ProfileCreateParams(TypedDict, total=False): name: str - """Optional name of the profile. Must be unique within the organization.""" + """Optional name of the profile. Must be unique within the project.""" From b98ed1a8c3de498a791dda3eb990d65010149f56 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 9 May 2026 03:18:39 +0000 Subject: [PATCH 385/448] fix(client): add missing f-string prefix in file type error message --- src/kernel/_files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kernel/_files.py b/src/kernel/_files.py index f30ac58a..3fc9f62e 100644 --- a/src/kernel/_files.py +++ b/src/kernel/_files.py @@ -99,7 +99,7 @@ async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles elif is_sequence_t(files): files = [(key, await _async_transform_file(file)) for key, file in files] else: - raise TypeError("Unexpected file type input {type(files)}, expected mapping or sequence") + raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence") return files From beac2dfddc3e9ff1d77d92a6b0ed51beaa1d58c7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 20:42:37 +0000 Subject: [PATCH 386/448] feat: browser_pools: add start_url config (KERNEL-1217 PR 2) --- .stats.yml | 4 +-- src/kernel/resources/browser_pools.py | 32 +++++++++++++++++++ src/kernel/resources/browsers/browsers.py | 16 ++++++++++ src/kernel/types/browser_create_params.py | 9 ++++++ src/kernel/types/browser_create_response.py | 6 ++++ src/kernel/types/browser_list_response.py | 6 ++++ src/kernel/types/browser_pool.py | 9 ++++++ .../types/browser_pool_acquire_response.py | 6 ++++ .../types/browser_pool_create_params.py | 9 ++++++ .../types/browser_pool_update_params.py | 9 ++++++ src/kernel/types/browser_retrieve_response.py | 6 ++++ src/kernel/types/browser_update_response.py | 6 ++++ .../invocation_list_browsers_response.py | 6 ++++ tests/api_resources/test_browser_pools.py | 4 +++ tests/api_resources/test_browsers.py | 2 ++ 15 files changed, 128 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 797624b4..e7699746 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-b51c72a040c8dfea9c0693de6631feabfffe42922d5feb60b4ac0ee5c83bb8f4.yml -openapi_spec_hash: 8b671cfe4debe8d9ad7c39ba5b0eaf6d +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-7d2d29d7598105d50e5118e0bc5ac654361a92cb95555705dbd1d236848e1456.yml +openapi_spec_hash: 10002eae793e08f81932239bbc72b503 config_hash: 08d55086449943a8fec212b870061a3f diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py index 6c265cd6..10b23923 100644 --- a/src/kernel/resources/browser_pools.py +++ b/src/kernel/resources/browser_pools.py @@ -68,6 +68,7 @@ def create( name: str | Omit = omit, profile: BrowserProfile | Omit = omit, proxy_id: str | Omit = omit, + start_url: str | Omit = omit, stealth: bool | Omit = omit, timeout_seconds: int | Omit = omit, viewport: BrowserViewport | Omit = omit, @@ -109,6 +110,12 @@ def create( proxy_id: Optional proxy to associate to the browser session. Must reference a proxy belonging to the caller's org. + start_url: Optional URL to navigate to when a new browser is warmed into the pool. + Best-effort: failures to navigate do not fail pool fill. Only applied to + newly-warmed browsers — browsers reused via release/acquire keep whatever URL + the previous lease left them on. Accepts any URL Chromium can resolve, including + chrome:// pages. + stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. @@ -149,6 +156,7 @@ def create( "name": name, "profile": profile, "proxy_id": proxy_id, + "start_url": start_url, "stealth": stealth, "timeout_seconds": timeout_seconds, "viewport": viewport, @@ -208,6 +216,7 @@ def update( name: str | Omit = omit, profile: BrowserProfile | Omit = omit, proxy_id: str | Omit = omit, + start_url: str | Omit = omit, stealth: bool | Omit = omit, timeout_seconds: int | Omit = omit, viewport: BrowserViewport | Omit = omit, @@ -252,6 +261,12 @@ def update( proxy_id: Optional proxy to associate to the browser session. Must reference a proxy belonging to the caller's org. + start_url: Optional URL to navigate to when a new browser is warmed into the pool. + Best-effort: failures to navigate do not fail pool fill. Only applied to + newly-warmed browsers — browsers reused via release/acquire keep whatever URL + the previous lease left them on. Accepts any URL Chromium can resolve, including + chrome:// pages. + stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. @@ -295,6 +310,7 @@ def update( "name": name, "profile": profile, "proxy_id": proxy_id, + "start_url": start_url, "stealth": stealth, "timeout_seconds": timeout_seconds, "viewport": viewport, @@ -530,6 +546,7 @@ async def create( name: str | Omit = omit, profile: BrowserProfile | Omit = omit, proxy_id: str | Omit = omit, + start_url: str | Omit = omit, stealth: bool | Omit = omit, timeout_seconds: int | Omit = omit, viewport: BrowserViewport | Omit = omit, @@ -571,6 +588,12 @@ async def create( proxy_id: Optional proxy to associate to the browser session. Must reference a proxy belonging to the caller's org. + start_url: Optional URL to navigate to when a new browser is warmed into the pool. + Best-effort: failures to navigate do not fail pool fill. Only applied to + newly-warmed browsers — browsers reused via release/acquire keep whatever URL + the previous lease left them on. Accepts any URL Chromium can resolve, including + chrome:// pages. + stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. @@ -611,6 +634,7 @@ async def create( "name": name, "profile": profile, "proxy_id": proxy_id, + "start_url": start_url, "stealth": stealth, "timeout_seconds": timeout_seconds, "viewport": viewport, @@ -670,6 +694,7 @@ async def update( name: str | Omit = omit, profile: BrowserProfile | Omit = omit, proxy_id: str | Omit = omit, + start_url: str | Omit = omit, stealth: bool | Omit = omit, timeout_seconds: int | Omit = omit, viewport: BrowserViewport | Omit = omit, @@ -714,6 +739,12 @@ async def update( proxy_id: Optional proxy to associate to the browser session. Must reference a proxy belonging to the caller's org. + start_url: Optional URL to navigate to when a new browser is warmed into the pool. + Best-effort: failures to navigate do not fail pool fill. Only applied to + newly-warmed browsers — browsers reused via release/acquire keep whatever URL + the previous lease left them on. Accepts any URL Chromium can resolve, including + chrome:// pages. + stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. @@ -757,6 +788,7 @@ async def update( "name": name, "profile": profile, "proxy_id": proxy_id, + "start_url": start_url, "stealth": stealth, "timeout_seconds": timeout_seconds, "viewport": viewport, diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 228e653a..c18448c7 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -153,6 +153,7 @@ def create( persistence: BrowserPersistenceParam | Omit = omit, profile: BrowserProfile | Omit = omit, proxy_id: str | Omit = omit, + start_url: str | Omit = omit, stealth: bool | Omit = omit, timeout_seconds: int | Omit = omit, viewport: BrowserViewport | Omit = omit, @@ -189,6 +190,12 @@ def create( proxy_id: Optional proxy to associate to the browser session. Must reference a proxy belonging to the caller's org. + start_url: Optional URL to navigate to immediately after the browser is created. + Best-effort: failures to navigate do not fail browser creation. Any pre-existing + tabs are reduced to a single tab which is then navigated. Accepts any URL + Chromium can resolve, including chrome:// pages. Ignored when reusing an + existing persistent session. + stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. @@ -231,6 +238,7 @@ def create( "persistence": persistence, "profile": profile, "proxy_id": proxy_id, + "start_url": start_url, "stealth": stealth, "timeout_seconds": timeout_seconds, "viewport": viewport, @@ -653,6 +661,7 @@ async def create( persistence: BrowserPersistenceParam | Omit = omit, profile: BrowserProfile | Omit = omit, proxy_id: str | Omit = omit, + start_url: str | Omit = omit, stealth: bool | Omit = omit, timeout_seconds: int | Omit = omit, viewport: BrowserViewport | Omit = omit, @@ -689,6 +698,12 @@ async def create( proxy_id: Optional proxy to associate to the browser session. Must reference a proxy belonging to the caller's org. + start_url: Optional URL to navigate to immediately after the browser is created. + Best-effort: failures to navigate do not fail browser creation. Any pre-existing + tabs are reduced to a single tab which is then navigated. Accepts any URL + Chromium can resolve, including chrome:// pages. Ignored when reusing an + existing persistent session. + stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. @@ -731,6 +746,7 @@ async def create( "persistence": persistence, "profile": profile, "proxy_id": proxy_id, + "start_url": start_url, "stealth": stealth, "timeout_seconds": timeout_seconds, "viewport": viewport, diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index 2827b1dc..1a4493f0 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -57,6 +57,15 @@ class BrowserCreateParams(TypedDict, total=False): Must reference a proxy belonging to the caller's org. """ + start_url: str + """Optional URL to navigate to immediately after the browser is created. + + Best-effort: failures to navigate do not fail browser creation. Any pre-existing + tabs are reduced to a single tab which is then navigated. Accepts any URL + Chromium can resolve, including chrome:// pages. Ignored when reusing an + existing persistent session. + """ + stealth: bool """ If true, launches the browser in stealth mode to reduce detection by anti-bot diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index 9356bb05..e63a6896 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -68,6 +68,12 @@ class BrowserCreateResponse(BaseModel): proxy_id: Optional[str] = None """ID of the proxy associated with this browser session, if any.""" + start_url: Optional[str] = None + """URL the session was asked to navigate to on creation, if any. + + Recorded for debugging — navigation is best-effort and may have failed. + """ + usage: Optional[BrowserUsage] = None """Session usage metrics.""" diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index f3a88f29..de37e26f 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -68,6 +68,12 @@ class BrowserListResponse(BaseModel): proxy_id: Optional[str] = None """ID of the proxy associated with this browser session, if any.""" + start_url: Optional[str] = None + """URL the session was asked to navigate to on creation, if any. + + Recorded for debugging — navigation is best-effort and may have failed. + """ + usage: Optional[BrowserUsage] = None """Session usage metrics.""" diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py index 807c73b6..a691d0eb 100644 --- a/src/kernel/types/browser_pool.py +++ b/src/kernel/types/browser_pool.py @@ -63,6 +63,15 @@ class BrowserPoolConfig(BaseModel): Must reference a proxy belonging to the caller's org. """ + start_url: Optional[str] = None + """Optional URL to navigate to when a new browser is warmed into the pool. + + Best-effort: failures to navigate do not fail pool fill. Only applied to + newly-warmed browsers — browsers reused via release/acquire keep whatever URL + the previous lease left them on. Accepts any URL Chromium can resolve, including + chrome:// pages. + """ + stealth: Optional[bool] = None """ If true, launches the browser in stealth mode to reduce detection by anti-bot diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py index 064c405d..cff8286d 100644 --- a/src/kernel/types/browser_pool_acquire_response.py +++ b/src/kernel/types/browser_pool_acquire_response.py @@ -68,6 +68,12 @@ class BrowserPoolAcquireResponse(BaseModel): proxy_id: Optional[str] = None """ID of the proxy associated with this browser session, if any.""" + start_url: Optional[str] = None + """URL the session was asked to navigate to on creation, if any. + + Recorded for debugging — navigation is best-effort and may have failed. + """ + usage: Optional[BrowserUsage] = None """Session usage metrics.""" diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py index cab4e138..99d6c9cd 100644 --- a/src/kernel/types/browser_pool_create_params.py +++ b/src/kernel/types/browser_pool_create_params.py @@ -62,6 +62,15 @@ class BrowserPoolCreateParams(TypedDict, total=False): Must reference a proxy belonging to the caller's org. """ + start_url: str + """Optional URL to navigate to when a new browser is warmed into the pool. + + Best-effort: failures to navigate do not fail pool fill. Only applied to + newly-warmed browsers — browsers reused via release/acquire keep whatever URL + the previous lease left them on. Accepts any URL Chromium can resolve, including + chrome:// pages. + """ + stealth: bool """ If true, launches the browser in stealth mode to reduce detection by anti-bot diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py index 7ddd1310..5b069c4c 100644 --- a/src/kernel/types/browser_pool_update_params.py +++ b/src/kernel/types/browser_pool_update_params.py @@ -68,6 +68,15 @@ class BrowserPoolUpdateParams(TypedDict, total=False): Must reference a proxy belonging to the caller's org. """ + start_url: str + """Optional URL to navigate to when a new browser is warmed into the pool. + + Best-effort: failures to navigate do not fail pool fill. Only applied to + newly-warmed browsers — browsers reused via release/acquire keep whatever URL + the previous lease left them on. Accepts any URL Chromium can resolve, including + chrome:// pages. + """ + stealth: bool """ If true, launches the browser in stealth mode to reduce detection by anti-bot diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 5b5a8913..80a96d36 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -68,6 +68,12 @@ class BrowserRetrieveResponse(BaseModel): proxy_id: Optional[str] = None """ID of the proxy associated with this browser session, if any.""" + start_url: Optional[str] = None + """URL the session was asked to navigate to on creation, if any. + + Recorded for debugging — navigation is best-effort and may have failed. + """ + usage: Optional[BrowserUsage] = None """Session usage metrics.""" diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py index 188895ad..4d1f61bc 100644 --- a/src/kernel/types/browser_update_response.py +++ b/src/kernel/types/browser_update_response.py @@ -68,6 +68,12 @@ class BrowserUpdateResponse(BaseModel): proxy_id: Optional[str] = None """ID of the proxy associated with this browser session, if any.""" + start_url: Optional[str] = None + """URL the session was asked to navigate to on creation, if any. + + Recorded for debugging — navigation is best-effort and may have failed. + """ + usage: Optional[BrowserUsage] = None """Session usage metrics.""" diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py index 23eda779..71c22a7a 100644 --- a/src/kernel/types/invocation_list_browsers_response.py +++ b/src/kernel/types/invocation_list_browsers_response.py @@ -68,6 +68,12 @@ class Browser(BaseModel): proxy_id: Optional[str] = None """ID of the proxy associated with this browser session, if any.""" + start_url: Optional[str] = None + """URL the session was asked to navigate to on creation, if any. + + Recorded for debugging — navigation is best-effort and may have failed. + """ + usage: Optional[BrowserUsage] = None """Session usage metrics.""" diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py index 70c7ad85..959eeef2 100644 --- a/tests/api_resources/test_browser_pools.py +++ b/tests/api_resources/test_browser_pools.py @@ -51,6 +51,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: "save_changes": True, }, proxy_id="proxy_id", + start_url="https://example.com", stealth=True, timeout_seconds=60, viewport={ @@ -162,6 +163,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None: "save_changes": True, }, proxy_id="proxy_id", + start_url="https://example.com", stealth=True, timeout_seconds=60, viewport={ @@ -473,6 +475,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> "save_changes": True, }, proxy_id="proxy_id", + start_url="https://example.com", stealth=True, timeout_seconds=60, viewport={ @@ -584,6 +587,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> "save_changes": True, }, proxy_id="proxy_id", + start_url="https://example.com", stealth=True, timeout_seconds=60, viewport={ diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index 96742c72..94c52654 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -53,6 +53,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: "save_changes": True, }, proxy_id="proxy_id", + start_url="https://example.com", stealth=True, timeout_seconds=10, viewport={ @@ -478,6 +479,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> "save_changes": True, }, proxy_id="proxy_id", + start_url="https://example.com", stealth=True, timeout_seconds=10, viewport={ From 83ab079b89e09add6e6c281dfd8d98ac3f11bdc0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 03:10:40 +0000 Subject: [PATCH 387/448] feat(internal/types): support eagerly validating pydantic iterators --- src/kernel/_models.py | 80 +++++++++++++++++++++++++++++++++++++++++++ tests/test_models.py | 60 ++++++++++++++++++++++++++++++-- 2 files changed, 137 insertions(+), 3 deletions(-) diff --git a/src/kernel/_models.py b/src/kernel/_models.py index 29070e05..8c5ab260 100644 --- a/src/kernel/_models.py +++ b/src/kernel/_models.py @@ -25,7 +25,9 @@ ClassVar, Protocol, Required, + Annotated, ParamSpec, + TypeAlias, TypedDict, TypeGuard, final, @@ -79,7 +81,15 @@ from ._constants import RAW_RESPONSE_HEADER if TYPE_CHECKING: + from pydantic import GetCoreSchemaHandler, ValidatorFunctionWrapHandler + from pydantic_core import CoreSchema, core_schema from pydantic_core.core_schema import ModelField, ModelSchema, LiteralSchema, ModelFieldsSchema +else: + try: + from pydantic_core import CoreSchema, core_schema + except ImportError: + CoreSchema = None + core_schema = None __all__ = ["BaseModel", "GenericModel"] @@ -396,6 +406,76 @@ def model_dump_json( ) +class _EagerIterable(list[_T], Generic[_T]): + """ + Accepts any Iterable[T] input (including generators), consumes it + eagerly, and validates all items upfront. + + Validation preserves the original container type where possible + (e.g. a set[T] stays a set[T]). Serialization (model_dump / JSON) + always emits a list — round-tripping through model_dump() will not + restore the original container type. + """ + + @classmethod + def __get_pydantic_core_schema__( + cls, + source_type: Any, + handler: GetCoreSchemaHandler, + ) -> CoreSchema: + (item_type,) = get_args(source_type) or (Any,) + item_schema: CoreSchema = handler.generate_schema(item_type) + list_of_items_schema: CoreSchema = core_schema.list_schema(item_schema) + + return core_schema.no_info_wrap_validator_function( + cls._validate, + list_of_items_schema, + serialization=core_schema.plain_serializer_function_ser_schema( + cls._serialize, + info_arg=False, + ), + ) + + @staticmethod + def _validate(v: Iterable[_T], handler: "ValidatorFunctionWrapHandler") -> Any: + original_type: type[Any] = type(v) + + # Normalize to list so list_schema can validate each item + if isinstance(v, list): + items: list[_T] = v + else: + try: + items = list(v) + except TypeError as e: + raise TypeError("Value is not iterable") from e + + # Validate items against the inner schema + validated: list[_T] = handler(items) + + # Reconstruct original container type + if original_type is list: + return validated + # str(list) produces the list's repr, not a string built from items, + # so skip reconstruction for str and its subclasses. + if issubclass(original_type, str): + return validated + try: + return original_type(validated) + except (TypeError, ValueError): + # If the type cannot be reconstructed, just return the validated list + return validated + + @staticmethod + def _serialize(v: Iterable[_T]) -> list[_T]: + """Always serialize as a list so Pydantic's JSON encoder is happy.""" + if isinstance(v, list): + return v + return list(v) + + +EagerIterable: TypeAlias = Annotated[Iterable[_T], _EagerIterable] + + def _construct_field(value: object, field: FieldInfo, key: str) -> object: if value is None: return field_get_default(field) diff --git a/tests/test_models.py b/tests/test_models.py index 78f0fd32..c3922db2 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,7 +1,8 @@ import json -from typing import TYPE_CHECKING, Any, Dict, List, Union, Optional, cast +from typing import TYPE_CHECKING, Any, Dict, List, Union, Iterable, Optional, cast from datetime import datetime, timezone -from typing_extensions import Literal, Annotated, TypeAliasType +from collections import deque +from typing_extensions import Literal, Annotated, TypedDict, TypeAliasType import pytest import pydantic @@ -9,7 +10,7 @@ from kernel._utils import PropertyInfo from kernel._compat import PYDANTIC_V1, parse_obj, model_dump, model_json -from kernel._models import DISCRIMINATOR_CACHE, BaseModel, construct_type +from kernel._models import DISCRIMINATOR_CACHE, BaseModel, EagerIterable, construct_type class BasicModel(BaseModel): @@ -961,3 +962,56 @@ def __getattr__(self, attr: str) -> Item: ... assert model.a.prop == 1 assert isinstance(model.a, Item) assert model.other == "foo" + + +# NOTE: Workaround for Pydantic Iterable behavior. +# Iterable fields are replaced with a ValidatorIterator and may be consumed +# during serialization, which can cause subsequent dumps to return empty data. +# See: https://github.com/pydantic/pydantic/issues/9541 +@pytest.mark.parametrize( + "data, expected_validated", + [ + ([1, 2, 3], [1, 2, 3]), + ((1, 2, 3), (1, 2, 3)), + (set([1, 2, 3]), set([1, 2, 3])), + (iter([1, 2, 3]), [1, 2, 3]), + ([], []), + ((x for x in [1, 2, 3]), [1, 2, 3]), + (map(lambda x: x, [1, 2, 3]), [1, 2, 3]), + (frozenset([1, 2, 3]), frozenset([1, 2, 3])), + (deque([1, 2, 3]), deque([1, 2, 3])), + ], + ids=["list", "tuple", "set", "iterator", "empty", "generator", "map", "frozenset", "deque"], +) +@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2") +def test_iterable_construction(data: Iterable[int], expected_validated: Iterable[int]) -> None: + class TypeWithIterable(TypedDict): + items: EagerIterable[int] + + class Model(BaseModel): + data: TypeWithIterable + + m = Model.model_validate({"data": {"items": data}}) + assert m.data["items"] == expected_validated + + # Verify repeated dumps don't lose data (the original bug) + assert m.model_dump()["data"]["items"] == list(expected_validated) + assert m.model_dump()["data"]["items"] == list(expected_validated) + + +@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2") +def test_iterable_construction_str_falls_back_to_list() -> None: + # str is iterable (over chars), but str(list_of_chars) produces the list's repr + # rather than reconstructing a string from items. We special-case str to fall + # back to list instead of attempting reconstruction. + class TypeWithIterable(TypedDict): + items: EagerIterable[str] + + class Model(BaseModel): + data: TypeWithIterable + + m = Model.model_validate({"data": {"items": "hello"}}) + + # falls back to list of chars rather than calling str(["h", "e", "l", "l", "o"]) + assert m.data["items"] == ["h", "e", "l", "l", "o"] + assert m.model_dump()["data"]["items"] == ["h", "e", "l", "l", "o"] From ceef7adf5cd4c5fe6b4892284784cb092a14d658 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 19:36:25 +0000 Subject: [PATCH 388/448] feat: managed-auth: surface awaiting_external_action even when fallback actions exist --- .stats.yml | 4 ++-- .../types/auth/connection_follow_response.py | 17 ++++++++++++----- src/kernel/types/auth/managed_auth.py | 17 ++++++++++++----- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/.stats.yml b/.stats.yml index e7699746..2085eb07 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-7d2d29d7598105d50e5118e0bc5ac654361a92cb95555705dbd1d236848e1456.yml -openapi_spec_hash: 10002eae793e08f81932239bbc72b503 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-38c6ce8b0ce54e6c62517b9635acaf65369c26cdb1b55eb66290c13a8ab2b33d.yml +openapi_spec_hash: e6f6ed157b1e318d1a1db72fd1d27091 config_hash: 08d55086449943a8fec212b870061a3f diff --git a/src/kernel/types/auth/connection_follow_response.py b/src/kernel/types/auth/connection_follow_response.py index 7cabc1c8..1cd1a141 100644 --- a/src/kernel/types/auth/connection_follow_response.py +++ b/src/kernel/types/auth/connection_follow_response.py @@ -119,7 +119,10 @@ class ManagedAuthStateEvent(BaseModel): """Time the state was reported.""" discovered_fields: Optional[List[ManagedAuthStateEventDiscoveredField]] = None - """Fields awaiting input (present when flow_step=AWAITING_INPUT).""" + """ + Fields awaiting input (present when flow_step=AWAITING_INPUT; may also be + present with AWAITING_EXTERNAL_ACTION as fallback actions). + """ error_code: Optional[str] = None """Machine-readable error code (present when flow_status=FAILED).""" @@ -144,12 +147,15 @@ class ManagedAuthStateEvent(BaseModel): mfa_options: Optional[List[ManagedAuthStateEventMfaOption]] = None """ - MFA method options (present when flow_step=AWAITING_INPUT and MFA selection - required). + MFA method options (present when flow_step=AWAITING_INPUT; may also be present + with AWAITING_EXTERNAL_ACTION as fallback actions). """ pending_sso_buttons: Optional[List[ManagedAuthStateEventPendingSSOButton]] = None - """SSO buttons available (present when flow_step=AWAITING_INPUT).""" + """ + SSO buttons available (present when flow_step=AWAITING_INPUT; may also be + present with AWAITING_EXTERNAL_ACTION as fallback actions). + """ post_login_url: Optional[str] = None """URL where the browser landed after successful login.""" @@ -157,7 +163,8 @@ class ManagedAuthStateEvent(BaseModel): sign_in_options: Optional[List[ManagedAuthStateEventSignInOption]] = None """ Non-MFA choices presented during the auth flow, such as account selection or org - pickers (present when flow_step=AWAITING_INPUT). + pickers (present when flow_step=AWAITING_INPUT; may also be present with + AWAITING_EXTERNAL_ACTION as fallback actions). """ website_error: Optional[str] = None diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py index 622c3bd5..c44fd8d1 100644 --- a/src/kernel/types/auth/managed_auth.py +++ b/src/kernel/types/auth/managed_auth.py @@ -185,7 +185,10 @@ class ManagedAuth(BaseModel): """ discovered_fields: Optional[List[DiscoveredField]] = None - """Fields awaiting input (present when flow_step=awaiting_input)""" + """ + Fields awaiting input (present when flow_step=awaiting_input; may also be + present with awaiting_external_action as fallback actions) + """ error_code: Optional[str] = None """Machine-readable error code (present when flow_status=failed)""" @@ -254,12 +257,15 @@ class ManagedAuth(BaseModel): mfa_options: Optional[List[MfaOption]] = None """ - MFA method options (present when flow_step=awaiting_input and MFA selection - required) + MFA method options (present when flow_step=awaiting_input; may also be present + with awaiting_external_action as fallback actions) """ pending_sso_buttons: Optional[List[PendingSSOButton]] = None - """SSO buttons available (present when flow_step=awaiting_input)""" + """ + SSO buttons available (present when flow_step=awaiting_input; may also be + present with awaiting_external_action as fallback actions) + """ post_login_url: Optional[str] = None """URL where the browser landed after successful login""" @@ -270,7 +276,8 @@ class ManagedAuth(BaseModel): sign_in_options: Optional[List[SignInOption]] = None """ Non-MFA choices presented during the auth flow, such as account selection or org - pickers (present when flow_step=awaiting_input). + pickers (present when flow_step=awaiting_input; may also be present with + awaiting_external_action as fallback actions). """ sso_provider: Optional[str] = None From b1dd9608053efb3a71e7499a488ce12b27987376 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 21:37:59 +0000 Subject: [PATCH 389/448] feat: Add opt-in record_session flag to managed auth --- .stats.yml | 4 +- src/kernel/resources/auth/connections.py | 42 ++++++++++++++++++- .../types/auth/connection_create_params.py | 6 +++ .../types/auth/connection_login_params.py | 6 +++ .../types/auth/connection_update_params.py | 3 ++ src/kernel/types/auth/managed_auth.py | 6 +++ tests/api_resources/auth/test_connections.py | 6 +++ 7 files changed, 69 insertions(+), 4 deletions(-) diff --git a/.stats.yml b/.stats.yml index 2085eb07..2939ac96 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-38c6ce8b0ce54e6c62517b9635acaf65369c26cdb1b55eb66290c13a8ab2b33d.yml -openapi_spec_hash: e6f6ed157b1e318d1a1db72fd1d27091 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-64dac369ae935b0318cd611e1735d45359a663eb55cf43fbb8b09e9dd814d7a2.yml +openapi_spec_hash: cc0c6a4e716977df4b9eab5f4658d41b config_hash: 08d55086449943a8fec212b870061a3f diff --git a/src/kernel/resources/auth/connections.py b/src/kernel/resources/auth/connections.py index fd28bd13..4befc6e9 100644 --- a/src/kernel/resources/auth/connections.py +++ b/src/kernel/resources/auth/connections.py @@ -66,6 +66,7 @@ def create( health_check_interval: int | Omit = omit, login_url: str | Omit = omit, proxy: connection_create_params.Proxy | Omit = omit, + record_session: bool | Omit = omit, save_credentials: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -122,6 +123,9 @@ def create( proxy: Proxy selection. Provide either id or name. The proxy must belong to the caller's org. + record_session: Whether to record browser sessions for this connection by default. Useful for + debugging. Can be overridden per-login. Defaults to false. + save_credentials: Whether to save credentials after every successful login. Defaults to true. One-time codes (TOTP, SMS, etc.) are not saved. @@ -144,6 +148,7 @@ def create( "health_check_interval": health_check_interval, "login_url": login_url, "proxy": proxy, + "record_session": record_session, "save_credentials": save_credentials, }, connection_create_params.ConnectionCreateParams, @@ -198,6 +203,7 @@ def update( health_check_interval: int | Omit = omit, login_url: str | Omit = omit, proxy: connection_update_params.Proxy | Omit = omit, + record_session: bool | Omit = omit, save_credentials: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -228,6 +234,8 @@ def update( proxy: Proxy selection. Provide either id or name. The proxy must belong to the caller's org. + record_session: Whether to record browser sessions for this connection by default + save_credentials: Whether to save credentials after every successful login extra_headers: Send extra headers @@ -249,6 +257,7 @@ def update( "health_check_interval": health_check_interval, "login_url": login_url, "proxy": proxy, + "record_session": record_session, "save_credentials": save_credentials, }, connection_update_params.ConnectionUpdateParams, @@ -398,6 +407,7 @@ def login( id: str, *, proxy: connection_login_params.Proxy | Omit = omit, + record_session: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -415,6 +425,9 @@ def login( proxy: Proxy selection. Provide either id or name. The proxy must belong to the caller's org. + record_session: Override the connection's default for recording this login's browser session. + When omitted, the connection's record_session default is used. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -427,7 +440,13 @@ def login( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( path_template("/auth/connections/{id}/login", id=id), - body=maybe_transform({"proxy": proxy}, connection_login_params.ConnectionLoginParams), + body=maybe_transform( + { + "proxy": proxy, + "record_session": record_session, + }, + connection_login_params.ConnectionLoginParams, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -529,6 +548,7 @@ async def create( health_check_interval: int | Omit = omit, login_url: str | Omit = omit, proxy: connection_create_params.Proxy | Omit = omit, + record_session: bool | Omit = omit, save_credentials: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -585,6 +605,9 @@ async def create( proxy: Proxy selection. Provide either id or name. The proxy must belong to the caller's org. + record_session: Whether to record browser sessions for this connection by default. Useful for + debugging. Can be overridden per-login. Defaults to false. + save_credentials: Whether to save credentials after every successful login. Defaults to true. One-time codes (TOTP, SMS, etc.) are not saved. @@ -607,6 +630,7 @@ async def create( "health_check_interval": health_check_interval, "login_url": login_url, "proxy": proxy, + "record_session": record_session, "save_credentials": save_credentials, }, connection_create_params.ConnectionCreateParams, @@ -661,6 +685,7 @@ async def update( health_check_interval: int | Omit = omit, login_url: str | Omit = omit, proxy: connection_update_params.Proxy | Omit = omit, + record_session: bool | Omit = omit, save_credentials: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -691,6 +716,8 @@ async def update( proxy: Proxy selection. Provide either id or name. The proxy must belong to the caller's org. + record_session: Whether to record browser sessions for this connection by default + save_credentials: Whether to save credentials after every successful login extra_headers: Send extra headers @@ -712,6 +739,7 @@ async def update( "health_check_interval": health_check_interval, "login_url": login_url, "proxy": proxy, + "record_session": record_session, "save_credentials": save_credentials, }, connection_update_params.ConnectionUpdateParams, @@ -861,6 +889,7 @@ async def login( id: str, *, proxy: connection_login_params.Proxy | Omit = omit, + record_session: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -878,6 +907,9 @@ async def login( proxy: Proxy selection. Provide either id or name. The proxy must belong to the caller's org. + record_session: Override the connection's default for recording this login's browser session. + When omitted, the connection's record_session default is used. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -890,7 +922,13 @@ async def login( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( path_template("/auth/connections/{id}/login", id=id), - body=await async_maybe_transform({"proxy": proxy}, connection_login_params.ConnectionLoginParams), + body=await async_maybe_transform( + { + "proxy": proxy, + "record_session": record_session, + }, + connection_login_params.ConnectionLoginParams, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/kernel/types/auth/connection_create_params.py b/src/kernel/types/auth/connection_create_params.py index b021994c..bdd22681 100644 --- a/src/kernel/types/auth/connection_create_params.py +++ b/src/kernel/types/auth/connection_create_params.py @@ -66,6 +66,12 @@ class ConnectionCreateParams(TypedDict, total=False): Provide either id or name. The proxy must belong to the caller's org. """ + record_session: bool + """Whether to record browser sessions for this connection by default. + + Useful for debugging. Can be overridden per-login. Defaults to false. + """ + save_credentials: bool """Whether to save credentials after every successful login. diff --git a/src/kernel/types/auth/connection_login_params.py b/src/kernel/types/auth/connection_login_params.py index 8ea6474c..39756cb1 100644 --- a/src/kernel/types/auth/connection_login_params.py +++ b/src/kernel/types/auth/connection_login_params.py @@ -14,6 +14,12 @@ class ConnectionLoginParams(TypedDict, total=False): Provide either id or name. The proxy must belong to the caller's org. """ + record_session: bool + """Override the connection's default for recording this login's browser session. + + When omitted, the connection's record_session default is used. + """ + class Proxy(TypedDict, total=False): """Proxy selection. diff --git a/src/kernel/types/auth/connection_update_params.py b/src/kernel/types/auth/connection_update_params.py index 77e738b7..23832778 100644 --- a/src/kernel/types/auth/connection_update_params.py +++ b/src/kernel/types/auth/connection_update_params.py @@ -33,6 +33,9 @@ class ConnectionUpdateParams(TypedDict, total=False): Provide either id or name. The proxy must belong to the caller's org. """ + record_session: bool + """Whether to record browser sessions for this connection by default""" + save_credentials: bool """Whether to save credentials after every successful login""" diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py index c44fd8d1..09f89deb 100644 --- a/src/kernel/types/auth/managed_auth.py +++ b/src/kernel/types/auth/managed_auth.py @@ -130,6 +130,12 @@ class ManagedAuth(BaseModel): profile_name: str """Name of the profile associated with this auth connection""" + record_session: bool + """ + Whether browser sessions for this connection are recorded by default for + debugging. Can be overridden per-login. + """ + save_credentials: bool """Whether credentials are saved after every successful login. diff --git a/tests/api_resources/auth/test_connections.py b/tests/api_resources/auth/test_connections.py index f5edd89d..e3da167f 100644 --- a/tests/api_resources/auth/test_connections.py +++ b/tests/api_resources/auth/test_connections.py @@ -50,6 +50,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: "id": "id", "name": "name", }, + record_session=False, save_credentials=True, ) assert_matches_type(ManagedAuth, connection, path=["response"]) @@ -150,6 +151,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None: "id": "id", "name": "name", }, + record_session=False, save_credentials=True, ) assert_matches_type(ManagedAuth, connection, path=["response"]) @@ -327,6 +329,7 @@ def test_method_login_with_all_params(self, client: Kernel) -> None: "id": "id", "name": "name", }, + record_session=True, ) assert_matches_type(LoginResponse, connection, path=["response"]) @@ -456,6 +459,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> "id": "id", "name": "name", }, + record_session=False, save_credentials=True, ) assert_matches_type(ManagedAuth, connection, path=["response"]) @@ -556,6 +560,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> "id": "id", "name": "name", }, + record_session=False, save_credentials=True, ) assert_matches_type(ManagedAuth, connection, path=["response"]) @@ -733,6 +738,7 @@ async def test_method_login_with_all_params(self, async_client: AsyncKernel) -> "id": "id", "name": "name", }, + record_session=True, ) assert_matches_type(LoginResponse, connection, path=["response"]) From f1dc0193097048352b202592a935317b70c36063 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 22:59:43 +0000 Subject: [PATCH 390/448] docs: clarify record_session description in OpenAPI spec --- .stats.yml | 4 ++-- src/kernel/types/auth/managed_auth.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.stats.yml b/.stats.yml index 2939ac96..7915e042 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-64dac369ae935b0318cd611e1735d45359a663eb55cf43fbb8b09e9dd814d7a2.yml -openapi_spec_hash: cc0c6a4e716977df4b9eab5f4658d41b +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-a33e59aa1758ba51f13538838ecd70b0a23ed69739b3022e8c2ce0622e42b904.yml +openapi_spec_hash: c042d2f6880c927be09aa9fa79d7241e config_hash: 08d55086449943a8fec212b870061a3f diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py index 09f89deb..980d0207 100644 --- a/src/kernel/types/auth/managed_auth.py +++ b/src/kernel/types/auth/managed_auth.py @@ -131,9 +131,9 @@ class ManagedAuth(BaseModel): """Name of the profile associated with this auth connection""" record_session: bool - """ - Whether browser sessions for this connection are recorded by default for - debugging. Can be overridden per-login. + """Whether to record browser session replays for this connection by default. + + Useful for debugging login flows. Can be overridden per-login. """ save_credentials: bool From 8420a0affb94faed09320fa46b5ab26314e42736 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 00:20:54 +0000 Subject: [PATCH 391/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index fed4b17f..c3e01e1e 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.52.0" + ".": "0.53.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 5d43220b..10375b3b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.52.0" +version = "0.53.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 1ce5f4bd..173fc2c2 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.52.0" # x-release-please-version +__version__ = "0.53.0" # x-release-please-version From f8db35b3d449bf8ea769fd9f30f2e7f0c677ec71 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 02:41:09 +0000 Subject: [PATCH 392/448] ci: pin GitHub Actions to commit SHAs Pin all GitHub Actions referenced in generated workflows (both first-party `actions/*` and third-party) to immutable commit SHAs. Updating pinned actions is now a deliberate codegen-side bump rather than implicit on every workflow run. --- .github/workflows/ci.yml | 8 ++++---- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/release-doctor.yml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fafebca7..ae545691 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/kernel-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rye run: | @@ -46,7 +46,7 @@ jobs: id-token: write runs-on: ${{ github.repository == 'stainless-sdks/kernel-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rye run: | @@ -67,7 +67,7 @@ jobs: github.repository == 'stainless-sdks/kernel-python' && !startsWith(github.ref, 'refs/heads/stl/') id: github-oidc - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: core.setOutput('github_token', await core.getIDToken()); @@ -87,7 +87,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/kernel-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rye run: | diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 2e95b5a9..2e7e7190 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rye run: | diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index 48941b69..057d4a7f 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'kernel/kernel-python-sdk' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Check release environment run: | From d7259187d711b02749cf79358e9d946a225170c3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 18:20:27 +0000 Subject: [PATCH 393/448] feat: Polish start URL OpenAPI descriptions --- .stats.yml | 4 +-- src/kernel/resources/browser_pools.py | 32 +++++++------------ src/kernel/resources/browsers/browsers.py | 16 ++++------ src/kernel/types/browser_create_params.py | 8 ++--- src/kernel/types/browser_create_response.py | 5 +-- src/kernel/types/browser_list_response.py | 5 +-- src/kernel/types/browser_pool.py | 8 ++--- .../types/browser_pool_acquire_response.py | 5 +-- .../types/browser_pool_create_params.py | 8 ++--- .../types/browser_pool_update_params.py | 8 ++--- src/kernel/types/browser_retrieve_response.py | 5 +-- src/kernel/types/browser_update_response.py | 5 +-- .../invocation_list_browsers_response.py | 5 +-- 13 files changed, 38 insertions(+), 76 deletions(-) diff --git a/.stats.yml b/.stats.yml index 7915e042..35cab471 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-a33e59aa1758ba51f13538838ecd70b0a23ed69739b3022e8c2ce0622e42b904.yml -openapi_spec_hash: c042d2f6880c927be09aa9fa79d7241e +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-b7a19ff1fbd93322c8cffcd0b397ce2536ca8bff91594e0081bd030d4bec879f.yml +openapi_spec_hash: 490520e6f0a8b1ebc89e9c0add46082d config_hash: 08d55086449943a8fec212b870061a3f diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py index 10b23923..682b2936 100644 --- a/src/kernel/resources/browser_pools.py +++ b/src/kernel/resources/browser_pools.py @@ -110,11 +110,9 @@ def create( proxy_id: Optional proxy to associate to the browser session. Must reference a proxy belonging to the caller's org. - start_url: Optional URL to navigate to when a new browser is warmed into the pool. - Best-effort: failures to navigate do not fail pool fill. Only applied to - newly-warmed browsers — browsers reused via release/acquire keep whatever URL - the previous lease left them on. Accepts any URL Chromium can resolve, including - chrome:// pages. + start_url: Optional URL to open when a browser is created for the pool. Navigation is + best-effort, so navigation failures do not prevent the pool from filling. Reused + browsers keep the page left by the previous lease. stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. @@ -261,11 +259,9 @@ def update( proxy_id: Optional proxy to associate to the browser session. Must reference a proxy belonging to the caller's org. - start_url: Optional URL to navigate to when a new browser is warmed into the pool. - Best-effort: failures to navigate do not fail pool fill. Only applied to - newly-warmed browsers — browsers reused via release/acquire keep whatever URL - the previous lease left them on. Accepts any URL Chromium can resolve, including - chrome:// pages. + start_url: Optional URL to open when a browser is created for the pool. Navigation is + best-effort, so navigation failures do not prevent the pool from filling. Reused + browsers keep the page left by the previous lease. stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. @@ -588,11 +584,9 @@ async def create( proxy_id: Optional proxy to associate to the browser session. Must reference a proxy belonging to the caller's org. - start_url: Optional URL to navigate to when a new browser is warmed into the pool. - Best-effort: failures to navigate do not fail pool fill. Only applied to - newly-warmed browsers — browsers reused via release/acquire keep whatever URL - the previous lease left them on. Accepts any URL Chromium can resolve, including - chrome:// pages. + start_url: Optional URL to open when a browser is created for the pool. Navigation is + best-effort, so navigation failures do not prevent the pool from filling. Reused + browsers keep the page left by the previous lease. stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. @@ -739,11 +733,9 @@ async def update( proxy_id: Optional proxy to associate to the browser session. Must reference a proxy belonging to the caller's org. - start_url: Optional URL to navigate to when a new browser is warmed into the pool. - Best-effort: failures to navigate do not fail pool fill. Only applied to - newly-warmed browsers — browsers reused via release/acquire keep whatever URL - the previous lease left them on. Accepts any URL Chromium can resolve, including - chrome:// pages. + start_url: Optional URL to open when a browser is created for the pool. Navigation is + best-effort, so navigation failures do not prevent the pool from filling. Reused + browsers keep the page left by the previous lease. stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index c18448c7..acf3d604 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -190,11 +190,9 @@ def create( proxy_id: Optional proxy to associate to the browser session. Must reference a proxy belonging to the caller's org. - start_url: Optional URL to navigate to immediately after the browser is created. - Best-effort: failures to navigate do not fail browser creation. Any pre-existing - tabs are reduced to a single tab which is then navigated. Accepts any URL - Chromium can resolve, including chrome:// pages. Ignored when reusing an - existing persistent session. + start_url: Optional URL to open when the browser session is created. Navigation is + best-effort, so navigation failures do not prevent the session from being + created. stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. @@ -698,11 +696,9 @@ async def create( proxy_id: Optional proxy to associate to the browser session. Must reference a proxy belonging to the caller's org. - start_url: Optional URL to navigate to immediately after the browser is created. - Best-effort: failures to navigate do not fail browser creation. Any pre-existing - tabs are reduced to a single tab which is then navigated. Accepts any URL - Chromium can resolve, including chrome:// pages. Ignored when reusing an - existing persistent session. + start_url: Optional URL to open when the browser session is created. Navigation is + best-effort, so navigation failures do not prevent the session from being + created. stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index 1a4493f0..399ded4c 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -58,12 +58,10 @@ class BrowserCreateParams(TypedDict, total=False): """ start_url: str - """Optional URL to navigate to immediately after the browser is created. + """Optional URL to open when the browser session is created. - Best-effort: failures to navigate do not fail browser creation. Any pre-existing - tabs are reduced to a single tab which is then navigated. Accepts any URL - Chromium can resolve, including chrome:// pages. Ignored when reusing an - existing persistent session. + Navigation is best-effort, so navigation failures do not prevent the session + from being created. """ stealth: bool diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index e63a6896..1c30ee63 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -69,10 +69,7 @@ class BrowserCreateResponse(BaseModel): """ID of the proxy associated with this browser session, if any.""" start_url: Optional[str] = None - """URL the session was asked to navigate to on creation, if any. - - Recorded for debugging — navigation is best-effort and may have failed. - """ + """Start URL requested for the session, if provided.""" usage: Optional[BrowserUsage] = None """Session usage metrics.""" diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index de37e26f..2a172671 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -69,10 +69,7 @@ class BrowserListResponse(BaseModel): """ID of the proxy associated with this browser session, if any.""" start_url: Optional[str] = None - """URL the session was asked to navigate to on creation, if any. - - Recorded for debugging — navigation is best-effort and may have failed. - """ + """Start URL requested for the session, if provided.""" usage: Optional[BrowserUsage] = None """Session usage metrics.""" diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py index a691d0eb..e456fca2 100644 --- a/src/kernel/types/browser_pool.py +++ b/src/kernel/types/browser_pool.py @@ -64,12 +64,10 @@ class BrowserPoolConfig(BaseModel): """ start_url: Optional[str] = None - """Optional URL to navigate to when a new browser is warmed into the pool. + """Optional URL to open when a browser is created for the pool. - Best-effort: failures to navigate do not fail pool fill. Only applied to - newly-warmed browsers — browsers reused via release/acquire keep whatever URL - the previous lease left them on. Accepts any URL Chromium can resolve, including - chrome:// pages. + Navigation is best-effort, so navigation failures do not prevent the pool from + filling. Reused browsers keep the page left by the previous lease. """ stealth: Optional[bool] = None diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py index cff8286d..91ea02e7 100644 --- a/src/kernel/types/browser_pool_acquire_response.py +++ b/src/kernel/types/browser_pool_acquire_response.py @@ -69,10 +69,7 @@ class BrowserPoolAcquireResponse(BaseModel): """ID of the proxy associated with this browser session, if any.""" start_url: Optional[str] = None - """URL the session was asked to navigate to on creation, if any. - - Recorded for debugging — navigation is best-effort and may have failed. - """ + """Start URL requested for the session, if provided.""" usage: Optional[BrowserUsage] = None """Session usage metrics.""" diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py index 99d6c9cd..bc08c3f2 100644 --- a/src/kernel/types/browser_pool_create_params.py +++ b/src/kernel/types/browser_pool_create_params.py @@ -63,12 +63,10 @@ class BrowserPoolCreateParams(TypedDict, total=False): """ start_url: str - """Optional URL to navigate to when a new browser is warmed into the pool. + """Optional URL to open when a browser is created for the pool. - Best-effort: failures to navigate do not fail pool fill. Only applied to - newly-warmed browsers — browsers reused via release/acquire keep whatever URL - the previous lease left them on. Accepts any URL Chromium can resolve, including - chrome:// pages. + Navigation is best-effort, so navigation failures do not prevent the pool from + filling. Reused browsers keep the page left by the previous lease. """ stealth: bool diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py index 5b069c4c..043eaf85 100644 --- a/src/kernel/types/browser_pool_update_params.py +++ b/src/kernel/types/browser_pool_update_params.py @@ -69,12 +69,10 @@ class BrowserPoolUpdateParams(TypedDict, total=False): """ start_url: str - """Optional URL to navigate to when a new browser is warmed into the pool. + """Optional URL to open when a browser is created for the pool. - Best-effort: failures to navigate do not fail pool fill. Only applied to - newly-warmed browsers — browsers reused via release/acquire keep whatever URL - the previous lease left them on. Accepts any URL Chromium can resolve, including - chrome:// pages. + Navigation is best-effort, so navigation failures do not prevent the pool from + filling. Reused browsers keep the page left by the previous lease. """ stealth: bool diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 80a96d36..63f4da9d 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -69,10 +69,7 @@ class BrowserRetrieveResponse(BaseModel): """ID of the proxy associated with this browser session, if any.""" start_url: Optional[str] = None - """URL the session was asked to navigate to on creation, if any. - - Recorded for debugging — navigation is best-effort and may have failed. - """ + """Start URL requested for the session, if provided.""" usage: Optional[BrowserUsage] = None """Session usage metrics.""" diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py index 4d1f61bc..2e30f49d 100644 --- a/src/kernel/types/browser_update_response.py +++ b/src/kernel/types/browser_update_response.py @@ -69,10 +69,7 @@ class BrowserUpdateResponse(BaseModel): """ID of the proxy associated with this browser session, if any.""" start_url: Optional[str] = None - """URL the session was asked to navigate to on creation, if any. - - Recorded for debugging — navigation is best-effort and may have failed. - """ + """Start URL requested for the session, if provided.""" usage: Optional[BrowserUsage] = None """Session usage metrics.""" diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py index 71c22a7a..c7b9e394 100644 --- a/src/kernel/types/invocation_list_browsers_response.py +++ b/src/kernel/types/invocation_list_browsers_response.py @@ -69,10 +69,7 @@ class Browser(BaseModel): """ID of the proxy associated with this browser session, if any.""" start_url: Optional[str] = None - """URL the session was asked to navigate to on creation, if any. - - Recorded for debugging — navigation is best-effort and may have failed. - """ + """Start URL requested for the session, if provided.""" usage: Optional[BrowserUsage] = None """Session usage metrics.""" From 9c6cc3c212e13c2a4545c6c1662e3d0d0f1069ae Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 20:32:29 +0000 Subject: [PATCH 394/448] feat: Add health check and auto-reauth controls for managed auth connections --- .stats.yml | 2 +- src/kernel/resources/auth/connections.py | 70 +++++++++++++++++++ .../types/auth/connection_create_params.py | 20 ++++++ .../types/auth/connection_update_params.py | 20 ++++++ src/kernel/types/auth/managed_auth.py | 21 ++++++ tests/api_resources/auth/test_connections.py | 8 +++ 6 files changed, 140 insertions(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 35cab471..6b78eac1 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 112 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-b7a19ff1fbd93322c8cffcd0b397ce2536ca8bff91594e0081bd030d4bec879f.yml -openapi_spec_hash: 490520e6f0a8b1ebc89e9c0add46082d +openapi_spec_hash: 9dd204b37a357b19032aea9eb4496645 config_hash: 08d55086449943a8fec212b870061a3f diff --git a/src/kernel/resources/auth/connections.py b/src/kernel/resources/auth/connections.py index 4befc6e9..dc912aba 100644 --- a/src/kernel/resources/auth/connections.py +++ b/src/kernel/resources/auth/connections.py @@ -62,8 +62,10 @@ def create( domain: str, profile_name: str, allowed_domains: SequenceNotStr[str] | Omit = omit, + auto_reauth: bool | Omit = omit, credential: connection_create_params.Credential | Omit = omit, health_check_interval: int | Omit = omit, + health_checks: bool | Omit = omit, login_url: str | Omit = omit, proxy: connection_create_params.Proxy | Omit = omit, record_session: bool | Omit = omit, @@ -105,6 +107,15 @@ def create( - OneLogin: \\**.onelogin.com - Ping Identity: _.pingone.com, _.pingidentity.com + auto_reauth: Whether to permit automatic re-authentication when a scheduled health check + detects an expired session. This is an opt-in flag only — it does not check + whether re-auth is actually feasible. Even when true, re-auth only runs when the + system has what it needs to perform it (for example, saved credentials for the + required login fields), and only after a scheduled health check detects an + expired session — so this flag has no effect when `health_checks` is false. When + false, expired sessions are marked as `NEEDS_AUTH` instead of attempting + re-auth. Defaults to true. + credential: Reference to credentials for the auth connection. Use one of: @@ -118,6 +129,11 @@ def create( depends on your plan: Enterprise: 300 (5 minutes), Startup: 1200 (20 minutes), Hobbyist: 3600 (1 hour). + health_checks: Whether to enable periodic health checks. When false, the system will not + automatically verify authentication status, and `auto_reauth` has no effect on + the automatic flow (since re-auth is only triggered by a failed scheduled health + check). Defaults to true. + login_url: Optional login page URL to skip discovery proxy: Proxy selection. Provide either id or name. The proxy must belong to the @@ -144,8 +160,10 @@ def create( "domain": domain, "profile_name": profile_name, "allowed_domains": allowed_domains, + "auto_reauth": auto_reauth, "credential": credential, "health_check_interval": health_check_interval, + "health_checks": health_checks, "login_url": login_url, "proxy": proxy, "record_session": record_session, @@ -199,8 +217,10 @@ def update( id: str, *, allowed_domains: SequenceNotStr[str] | Omit = omit, + auto_reauth: bool | Omit = omit, credential: connection_update_params.Credential | Omit = omit, health_check_interval: int | Omit = omit, + health_checks: bool | Omit = omit, login_url: str | Omit = omit, proxy: connection_update_params.Proxy | Omit = omit, record_session: bool | Omit = omit, @@ -220,6 +240,14 @@ def update( Args: allowed_domains: Additional domains valid for this auth flow (replaces existing list) + auto_reauth: Whether automatic re-authentication is permitted for this connection. This is an + opt-in flag only — it does not check whether re-auth is actually feasible. Even + when true, re-auth only runs when the system has what it needs to perform it + (for example, saved credentials for the required login fields), and only after a + scheduled health check detects an expired session — so this flag has no effect + when `health_checks` is false. When false, expired sessions detected by a health + check are marked as `NEEDS_AUTH` instead of attempting re-auth. + credential: Reference to credentials for the auth connection. Use one of: @@ -229,6 +257,11 @@ def update( health_check_interval: Interval in seconds between automatic health checks + health_checks: Whether periodic health checks are enabled. When set to false, the system will + not automatically verify authentication status, and `auto_reauth` has no effect + on the automatic flow (since re-auth is only triggered by a failed scheduled + health check). + login_url: Login page URL. Set to empty string to clear. proxy: Proxy selection. Provide either id or name. The proxy must belong to the @@ -253,8 +286,10 @@ def update( body=maybe_transform( { "allowed_domains": allowed_domains, + "auto_reauth": auto_reauth, "credential": credential, "health_check_interval": health_check_interval, + "health_checks": health_checks, "login_url": login_url, "proxy": proxy, "record_session": record_session, @@ -544,8 +579,10 @@ async def create( domain: str, profile_name: str, allowed_domains: SequenceNotStr[str] | Omit = omit, + auto_reauth: bool | Omit = omit, credential: connection_create_params.Credential | Omit = omit, health_check_interval: int | Omit = omit, + health_checks: bool | Omit = omit, login_url: str | Omit = omit, proxy: connection_create_params.Proxy | Omit = omit, record_session: bool | Omit = omit, @@ -587,6 +624,15 @@ async def create( - OneLogin: \\**.onelogin.com - Ping Identity: _.pingone.com, _.pingidentity.com + auto_reauth: Whether to permit automatic re-authentication when a scheduled health check + detects an expired session. This is an opt-in flag only — it does not check + whether re-auth is actually feasible. Even when true, re-auth only runs when the + system has what it needs to perform it (for example, saved credentials for the + required login fields), and only after a scheduled health check detects an + expired session — so this flag has no effect when `health_checks` is false. When + false, expired sessions are marked as `NEEDS_AUTH` instead of attempting + re-auth. Defaults to true. + credential: Reference to credentials for the auth connection. Use one of: @@ -600,6 +646,11 @@ async def create( depends on your plan: Enterprise: 300 (5 minutes), Startup: 1200 (20 minutes), Hobbyist: 3600 (1 hour). + health_checks: Whether to enable periodic health checks. When false, the system will not + automatically verify authentication status, and `auto_reauth` has no effect on + the automatic flow (since re-auth is only triggered by a failed scheduled health + check). Defaults to true. + login_url: Optional login page URL to skip discovery proxy: Proxy selection. Provide either id or name. The proxy must belong to the @@ -626,8 +677,10 @@ async def create( "domain": domain, "profile_name": profile_name, "allowed_domains": allowed_domains, + "auto_reauth": auto_reauth, "credential": credential, "health_check_interval": health_check_interval, + "health_checks": health_checks, "login_url": login_url, "proxy": proxy, "record_session": record_session, @@ -681,8 +734,10 @@ async def update( id: str, *, allowed_domains: SequenceNotStr[str] | Omit = omit, + auto_reauth: bool | Omit = omit, credential: connection_update_params.Credential | Omit = omit, health_check_interval: int | Omit = omit, + health_checks: bool | Omit = omit, login_url: str | Omit = omit, proxy: connection_update_params.Proxy | Omit = omit, record_session: bool | Omit = omit, @@ -702,6 +757,14 @@ async def update( Args: allowed_domains: Additional domains valid for this auth flow (replaces existing list) + auto_reauth: Whether automatic re-authentication is permitted for this connection. This is an + opt-in flag only — it does not check whether re-auth is actually feasible. Even + when true, re-auth only runs when the system has what it needs to perform it + (for example, saved credentials for the required login fields), and only after a + scheduled health check detects an expired session — so this flag has no effect + when `health_checks` is false. When false, expired sessions detected by a health + check are marked as `NEEDS_AUTH` instead of attempting re-auth. + credential: Reference to credentials for the auth connection. Use one of: @@ -711,6 +774,11 @@ async def update( health_check_interval: Interval in seconds between automatic health checks + health_checks: Whether periodic health checks are enabled. When set to false, the system will + not automatically verify authentication status, and `auto_reauth` has no effect + on the automatic flow (since re-auth is only triggered by a failed scheduled + health check). + login_url: Login page URL. Set to empty string to clear. proxy: Proxy selection. Provide either id or name. The proxy must belong to the @@ -735,8 +803,10 @@ async def update( body=await async_maybe_transform( { "allowed_domains": allowed_domains, + "auto_reauth": auto_reauth, "credential": credential, "health_check_interval": health_check_interval, + "health_checks": health_checks, "login_url": login_url, "proxy": proxy, "record_session": record_session, diff --git a/src/kernel/types/auth/connection_create_params.py b/src/kernel/types/auth/connection_create_params.py index bdd22681..acde944c 100644 --- a/src/kernel/types/auth/connection_create_params.py +++ b/src/kernel/types/auth/connection_create_params.py @@ -40,6 +40,18 @@ class ConnectionCreateParams(TypedDict, total=False): - Ping Identity: _.pingone.com, _.pingidentity.com """ + auto_reauth: bool + """ + Whether to permit automatic re-authentication when a scheduled health check + detects an expired session. This is an opt-in flag only — it does not check + whether re-auth is actually feasible. Even when true, re-auth only runs when the + system has what it needs to perform it (for example, saved credentials for the + required login fields), and only after a scheduled health check detects an + expired session — so this flag has no effect when `health_checks` is false. When + false, expired sessions are marked as `NEEDS_AUTH` instead of attempting + re-auth. Defaults to true. + """ + credential: Credential """Reference to credentials for the auth connection. Use one of: @@ -57,6 +69,14 @@ class ConnectionCreateParams(TypedDict, total=False): Startup: 1200 (20 minutes), Hobbyist: 3600 (1 hour). """ + health_checks: bool + """Whether to enable periodic health checks. + + When false, the system will not automatically verify authentication status, and + `auto_reauth` has no effect on the automatic flow (since re-auth is only + triggered by a failed scheduled health check). Defaults to true. + """ + login_url: str """Optional login page URL to skip discovery""" diff --git a/src/kernel/types/auth/connection_update_params.py b/src/kernel/types/auth/connection_update_params.py index 23832778..8875f609 100644 --- a/src/kernel/types/auth/connection_update_params.py +++ b/src/kernel/types/auth/connection_update_params.py @@ -13,6 +13,18 @@ class ConnectionUpdateParams(TypedDict, total=False): allowed_domains: SequenceNotStr[str] """Additional domains valid for this auth flow (replaces existing list)""" + auto_reauth: bool + """Whether automatic re-authentication is permitted for this connection. + + This is an opt-in flag only — it does not check whether re-auth is actually + feasible. Even when true, re-auth only runs when the system has what it needs to + perform it (for example, saved credentials for the required login fields), and + only after a scheduled health check detects an expired session — so this flag + has no effect when `health_checks` is false. When false, expired sessions + detected by a health check are marked as `NEEDS_AUTH` instead of attempting + re-auth. + """ + credential: Credential """Reference to credentials for the auth connection. Use one of: @@ -24,6 +36,14 @@ class ConnectionUpdateParams(TypedDict, total=False): health_check_interval: int """Interval in seconds between automatic health checks""" + health_checks: bool + """Whether periodic health checks are enabled. + + When set to false, the system will not automatically verify authentication + status, and `auto_reauth` has no effect on the automatic flow (since re-auth is + only triggered by a failed scheduled health check). + """ + login_url: str """Login page URL. Set to empty string to clear.""" diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py index 980d0207..893631d4 100644 --- a/src/kernel/types/auth/managed_auth.py +++ b/src/kernel/types/auth/managed_auth.py @@ -166,6 +166,18 @@ class ManagedAuth(BaseModel): - Ping Identity: _.pingone.com, _.pingidentity.com """ + auto_reauth: Optional[bool] = None + """Whether automatic re-authentication is permitted for this connection. + + This is an opt-in flag only — it does not check whether re-auth is actually + feasible. Even when true, re-auth only runs when the system has what it needs to + perform it (for example, saved credentials for the required login fields), and + only after a scheduled health check detects an expired session — so this flag + has no effect when `health_checks` is false. When false, expired sessions + detected by a health check are marked as `NEEDS_AUTH` instead of attempting + re-auth. + """ + browser_session_id: Optional[str] = None """ ID of the underlying browser session driving the current flow (present when flow @@ -236,6 +248,15 @@ class ManagedAuth(BaseModel): Startup: 1200 (20 minutes), Hobbyist: 3600 (1 hour). """ + health_checks: Optional[bool] = None + """Whether periodic health checks are enabled for this connection. + + When false, the system will not automatically verify authentication status, and + `auto_reauth` has no effect on the automatic flow (since re-auth is only + triggered by a failed scheduled health check). Manually triggering a health + check via the API still works regardless of this setting. + """ + hosted_url: Optional[str] = None """URL to redirect user to for hosted login (present when flow in progress)""" diff --git a/tests/api_resources/auth/test_connections.py b/tests/api_resources/auth/test_connections.py index e3da167f..702207fe 100644 --- a/tests/api_resources/auth/test_connections.py +++ b/tests/api_resources/auth/test_connections.py @@ -38,6 +38,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: domain="netflix.com", profile_name="user-123", allowed_domains=["login.netflix.com", "auth.netflix.com"], + auto_reauth=True, credential={ "auto": True, "name": "my-netflix-creds", @@ -45,6 +46,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: "provider": "my-1p", }, health_check_interval=3600, + health_checks=True, login_url="https://netflix.com/login", proxy={ "id": "id", @@ -139,6 +141,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None: connection = client.auth.connections.update( id="id", allowed_domains=["login.netflix.com", "auth.netflix.com"], + auto_reauth=True, credential={ "auto": True, "name": "my-netflix-creds", @@ -146,6 +149,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None: "provider": "my-1p", }, health_check_interval=3600, + health_checks=True, login_url="https://netflix.com/login", proxy={ "id": "id", @@ -447,6 +451,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> domain="netflix.com", profile_name="user-123", allowed_domains=["login.netflix.com", "auth.netflix.com"], + auto_reauth=True, credential={ "auto": True, "name": "my-netflix-creds", @@ -454,6 +459,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> "provider": "my-1p", }, health_check_interval=3600, + health_checks=True, login_url="https://netflix.com/login", proxy={ "id": "id", @@ -548,6 +554,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> connection = await async_client.auth.connections.update( id="id", allowed_domains=["login.netflix.com", "auth.netflix.com"], + auto_reauth=True, credential={ "auto": True, "name": "my-netflix-creds", @@ -555,6 +562,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> "provider": "my-1p", }, health_check_interval=3600, + health_checks=True, login_url="https://netflix.com/login", proxy={ "id": "id", From 20c8c0c3bd24b219c9c1aea4d96cb7406b28bb1a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 20:58:13 +0000 Subject: [PATCH 395/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index c3e01e1e..d940b600 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.53.0" + ".": "0.55.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 10375b3b..353f3cd9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.53.0" +version = "0.55.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 173fc2c2..b2f051b4 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.53.0" # x-release-please-version +__version__ = "0.55.0" # x-release-please-version From a62c4fbcd17fe76f243eaaf6b17c5ff6f011a098 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 02:13:51 +0000 Subject: [PATCH 396/448] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 6b78eac1..e7238e0a 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-b7a19ff1fbd93322c8cffcd0b397ce2536ca8bff91594e0081bd030d4bec879f.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-85604d59d55171797ed7e5e60f46532e7825ccfcaf6ee1319284d513f582a9cf.yml openapi_spec_hash: 9dd204b37a357b19032aea9eb4496645 config_hash: 08d55086449943a8fec212b870061a3f From d84e353b413f352fb1eb0b9a8d83c258e97f4122 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 13:14:44 +0000 Subject: [PATCH 397/448] feat: Expose POST /projects in public API --- .stats.yml | 4 ++-- src/kernel/resources/projects/projects.py | 12 ++++-------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/.stats.yml b/.stats.yml index e7238e0a..e0a033ee 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-85604d59d55171797ed7e5e60f46532e7825ccfcaf6ee1319284d513f582a9cf.yml -openapi_spec_hash: 9dd204b37a357b19032aea9eb4496645 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-982efd191c23737c9e1cdbcbf9237fa2231b9f74e0a25db2870293bdf9951c21.yml +openapi_spec_hash: eeb27952a4cc939316c24fc0ce2c9e3a config_hash: 08d55086449943a8fec212b870061a3f diff --git a/src/kernel/resources/projects/projects.py b/src/kernel/resources/projects/projects.py index f73e70d7..87fae1b8 100644 --- a/src/kernel/resources/projects/projects.py +++ b/src/kernel/resources/projects/projects.py @@ -70,10 +70,8 @@ def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Project: - """Create a new project within the authenticated organization. - - Requires the - projects feature flag. + """ + Create a new project within the authenticated organization. Args: name: Project name (1-255 characters) @@ -299,10 +297,8 @@ async def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Project: - """Create a new project within the authenticated organization. - - Requires the - projects feature flag. + """ + Create a new project within the authenticated organization. Args: name: Project name (1-255 characters) From 824b72952fc250f0521db21f02f5264b225a8702 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 17:26:16 +0000 Subject: [PATCH 398/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d940b600..87d3d84c 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.55.0" + ".": "0.56.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 353f3cd9..c98902d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.55.0" +version = "0.56.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index b2f051b4..a4d2a572 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.55.0" # x-release-please-version +__version__ = "0.56.0" # x-release-please-version From b5449b1cf31a035c7b50bf352df461da8cfbfa85 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 00:02:33 +0000 Subject: [PATCH 399/448] feat: browsers: accept chrome_policy on POST /browsers (KERNEL-1216) --- .stats.yml | 4 ++-- src/kernel/resources/browsers/browsers.py | 16 ++++++++++++++++ src/kernel/types/browser_create_params.py | 11 ++++++++++- src/kernel/types/browser_create_response.py | 9 ++++++++- src/kernel/types/browser_list_response.py | 9 ++++++++- .../types/browser_pool_acquire_response.py | 9 ++++++++- src/kernel/types/browser_retrieve_response.py | 9 ++++++++- src/kernel/types/browser_update_response.py | 9 ++++++++- .../types/invocation_list_browsers_response.py | 9 ++++++++- tests/api_resources/test_browsers.py | 2 ++ 10 files changed, 78 insertions(+), 9 deletions(-) diff --git a/.stats.yml b/.stats.yml index e0a033ee..542eaa0d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-982efd191c23737c9e1cdbcbf9237fa2231b9f74e0a25db2870293bdf9951c21.yml -openapi_spec_hash: eeb27952a4cc939316c24fc0ce2c9e3a +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e564e74a7aae1744d4aa070a63c387f456c4719a48747dc6229b58a986255b65.yml +openapi_spec_hash: 62beb1f20708652aaee31bbffb6cfbe9 config_hash: 08d55086449943a8fec212b870061a3f diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index acf3d604..9a8544ce 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -145,6 +145,7 @@ def with_streaming_response(self) -> BrowsersResourceWithStreamingResponse: def create( self, *, + chrome_policy: Dict[str, object] | Omit = omit, extensions: Iterable[BrowserExtension] | Omit = omit, gpu: bool | Omit = omit, headless: bool | Omit = omit, @@ -168,6 +169,12 @@ def create( Create a new browser session from within an action. Args: + chrome_policy: Custom Chrome enterprise policy overrides applied to this browser session. Keys + are Chrome enterprise policy names; values must match their expected types. + Blocked: kernel-managed policies (extensions, proxy, CDP/automation). Ignored + when reusing an existing persistent session. See + https://chromeenterprise.google/policies/ + extensions: List of browser extensions to load into the session. Provide each by id or name. gpu: If true, enables GPU acceleration for the browser session. Requires Start-Up or @@ -228,6 +235,7 @@ def create( "/browsers", body=maybe_transform( { + "chrome_policy": chrome_policy, "extensions": extensions, "gpu": gpu, "headless": headless, @@ -651,6 +659,7 @@ def with_streaming_response(self) -> AsyncBrowsersResourceWithStreamingResponse: async def create( self, *, + chrome_policy: Dict[str, object] | Omit = omit, extensions: Iterable[BrowserExtension] | Omit = omit, gpu: bool | Omit = omit, headless: bool | Omit = omit, @@ -674,6 +683,12 @@ async def create( Create a new browser session from within an action. Args: + chrome_policy: Custom Chrome enterprise policy overrides applied to this browser session. Keys + are Chrome enterprise policy names; values must match their expected types. + Blocked: kernel-managed policies (extensions, proxy, CDP/automation). Ignored + when reusing an existing persistent session. See + https://chromeenterprise.google/policies/ + extensions: List of browser extensions to load into the session. Provide each by id or name. gpu: If true, enables GPU acceleration for the browser session. Requires Start-Up or @@ -734,6 +749,7 @@ async def create( "/browsers", body=await async_maybe_transform( { + "chrome_policy": chrome_policy, "extensions": extensions, "gpu": gpu, "headless": headless, diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index 399ded4c..c05fd94e 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Iterable +from typing import Dict, Iterable from typing_extensions import TypedDict from .browser_persistence_param import BrowserPersistenceParam @@ -14,6 +14,15 @@ class BrowserCreateParams(TypedDict, total=False): + chrome_policy: Dict[str, object] + """Custom Chrome enterprise policy overrides applied to this browser session. + + Keys are Chrome enterprise policy names; values must match their expected types. + Blocked: kernel-managed policies (extensions, proxy, CDP/automation). Ignored + when reusing an existing persistent session. See + https://chromeenterprise.google/policies/ + """ + extensions: Iterable[BrowserExtension] """List of browser extensions to load into the session. diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index 1c30ee63..2e2e5dbc 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional +from typing import Dict, Optional from datetime import datetime from .profile import Profile @@ -44,6 +44,13 @@ class BrowserCreateResponse(BaseModel): Only available for non-headless browsers. """ + chrome_policy: Optional[Dict[str, object]] = None + """ + Custom Chrome enterprise policy overrides that were applied to this browser + session, if any. Echoed back for verification. Keys are Chrome enterprise policy + names. + """ + deleted_at: Optional[datetime] = None """When the browser session was soft-deleted. Only present for deleted sessions.""" diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index 2a172671..27e409f4 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional +from typing import Dict, Optional from datetime import datetime from .profile import Profile @@ -44,6 +44,13 @@ class BrowserListResponse(BaseModel): Only available for non-headless browsers. """ + chrome_policy: Optional[Dict[str, object]] = None + """ + Custom Chrome enterprise policy overrides that were applied to this browser + session, if any. Echoed back for verification. Keys are Chrome enterprise policy + names. + """ + deleted_at: Optional[datetime] = None """When the browser session was soft-deleted. Only present for deleted sessions.""" diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py index 91ea02e7..9cf914ee 100644 --- a/src/kernel/types/browser_pool_acquire_response.py +++ b/src/kernel/types/browser_pool_acquire_response.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional +from typing import Dict, Optional from datetime import datetime from .profile import Profile @@ -44,6 +44,13 @@ class BrowserPoolAcquireResponse(BaseModel): Only available for non-headless browsers. """ + chrome_policy: Optional[Dict[str, object]] = None + """ + Custom Chrome enterprise policy overrides that were applied to this browser + session, if any. Echoed back for verification. Keys are Chrome enterprise policy + names. + """ + deleted_at: Optional[datetime] = None """When the browser session was soft-deleted. Only present for deleted sessions.""" diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 63f4da9d..95cfa9ad 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional +from typing import Dict, Optional from datetime import datetime from .profile import Profile @@ -44,6 +44,13 @@ class BrowserRetrieveResponse(BaseModel): Only available for non-headless browsers. """ + chrome_policy: Optional[Dict[str, object]] = None + """ + Custom Chrome enterprise policy overrides that were applied to this browser + session, if any. Echoed back for verification. Keys are Chrome enterprise policy + names. + """ + deleted_at: Optional[datetime] = None """When the browser session was soft-deleted. Only present for deleted sessions.""" diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py index 2e30f49d..209d58c4 100644 --- a/src/kernel/types/browser_update_response.py +++ b/src/kernel/types/browser_update_response.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional +from typing import Dict, Optional from datetime import datetime from .profile import Profile @@ -44,6 +44,13 @@ class BrowserUpdateResponse(BaseModel): Only available for non-headless browsers. """ + chrome_policy: Optional[Dict[str, object]] = None + """ + Custom Chrome enterprise policy overrides that were applied to this browser + session, if any. Echoed back for verification. Keys are Chrome enterprise policy + names. + """ + deleted_at: Optional[datetime] = None """When the browser session was soft-deleted. Only present for deleted sessions.""" diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py index c7b9e394..7ff4910a 100644 --- a/src/kernel/types/invocation_list_browsers_response.py +++ b/src/kernel/types/invocation_list_browsers_response.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List, Optional +from typing import Dict, List, Optional from datetime import datetime from .profile import Profile @@ -44,6 +44,13 @@ class Browser(BaseModel): Only available for non-headless browsers. """ + chrome_policy: Optional[Dict[str, object]] = None + """ + Custom Chrome enterprise policy overrides that were applied to this browser + session, if any. Echoed back for verification. Keys are Chrome enterprise policy + names. + """ + deleted_at: Optional[datetime] = None """When the browser session was soft-deleted. Only present for deleted sessions.""" diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index 94c52654..3d1ac191 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -36,6 +36,7 @@ def test_method_create(self, client: Kernel) -> None: @parametrize def test_method_create_with_all_params(self, client: Kernel) -> None: browser = client.browsers.create( + chrome_policy={"foo": "bar"}, extensions=[ { "id": "id", @@ -462,6 +463,7 @@ async def test_method_create(self, async_client: AsyncKernel) -> None: @parametrize async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: browser = await async_client.browsers.create( + chrome_policy={"foo": "bar"}, extensions=[ { "id": "id", From 782a53c37f6f9cc7ba20b206cef7f31dbc5d913c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 18:53:01 +0000 Subject: [PATCH 400/448] feat: [kernel-1116] browser events api integration --- .stats.yml | 8 +- api.md | 44 ++++ src/kernel/resources/browser_pools.py | 32 +-- src/kernel/resources/browsers/__init__.py | 14 ++ src/kernel/resources/browsers/browsers.py | 65 ++++++ src/kernel/resources/browsers/telemetry.py | 195 ++++++++++++++++++ src/kernel/resources/proxies.py | 8 +- src/kernel/types/__init__.py | 17 ++ src/kernel/types/browser_create_params.py | 10 +- src/kernel/types/browser_create_response.py | 12 +- src/kernel/types/browser_list_response.py | 12 +- src/kernel/types/browser_pool.py | 8 +- .../types/browser_pool_acquire_response.py | 12 +- .../types/browser_pool_create_params.py | 8 +- .../types/browser_pool_update_params.py | 8 +- src/kernel/types/browser_retrieve_response.py | 12 +- src/kernel/types/browser_update_params.py | 10 + src/kernel/types/browser_update_response.py | 12 +- src/kernel/types/browsers/__init__.py | 44 ++++ .../types/browsers/browser_call_stack.py | 43 ++++ .../browsers/browser_console_error_event.py | 87 ++++++++ .../browsers/browser_console_log_event.py | 64 ++++++ .../types/browsers/browser_event_context.py | 42 ++++ .../types/browsers/browser_event_source.py | 30 +++ .../types/browsers/browser_http_headers.py | 8 + .../browser_interaction_click_event.py | 54 +++++ .../browsers/browser_interaction_key_event.py | 48 +++++ ...rowser_interaction_scroll_settled_event.py | 56 +++++ .../browser_monitor_disconnected_event.py | 34 +++ .../browser_monitor_init_failed_event.py | 31 +++ .../browser_monitor_reconnect_failed_event.py | 37 ++++ .../browser_monitor_reconnected_event.py | 33 +++ .../browser_monitor_screenshot_event.py | 31 +++ .../browsers/browser_network_idle_event.py | 34 +++ .../browser_network_loading_failed_event.py | 58 ++++++ .../browsers/browser_network_request_event.py | 74 +++++++ .../browser_network_response_event.py | 71 +++++++ .../browser_page_dom_content_loaded_event.py | 45 ++++ .../browser_page_layout_settled_event.py | 34 +++ .../browser_page_layout_shift_event.py | 69 +++++++ .../types/browsers/browser_page_lcp_event.py | 75 +++++++ .../types/browsers/browser_page_load_event.py | 45 ++++ .../browsers/browser_page_navigation_event.py | 55 +++++ .../browser_page_navigation_settled_event.py | 34 +++ .../browsers/browser_page_tab_opened_event.py | 45 ++++ .../browser_telemetry_categories_config.py | 32 +++ ...owser_telemetry_categories_config_param.py | 33 +++ .../browser_telemetry_category_config.py | 14 ++ ...browser_telemetry_category_config_param.py | 14 ++ .../browsers/browser_telemetry_config.py | 15 ++ .../browser_telemetry_config_param.py | 16 ++ .../types/browsers/browser_telemetry_event.py | 61 ++++++ .../browsers/telemetry_stream_response.py | 34 +++ .../invocation_list_browsers_response.py | 12 +- src/kernel/types/proxy_check_params.py | 5 +- .../api_resources/browsers/test_telemetry.py | 122 +++++++++++ tests/api_resources/test_browsers.py | 32 +++ 57 files changed, 2123 insertions(+), 35 deletions(-) create mode 100644 src/kernel/resources/browsers/telemetry.py create mode 100644 src/kernel/types/browsers/browser_call_stack.py create mode 100644 src/kernel/types/browsers/browser_console_error_event.py create mode 100644 src/kernel/types/browsers/browser_console_log_event.py create mode 100644 src/kernel/types/browsers/browser_event_context.py create mode 100644 src/kernel/types/browsers/browser_event_source.py create mode 100644 src/kernel/types/browsers/browser_http_headers.py create mode 100644 src/kernel/types/browsers/browser_interaction_click_event.py create mode 100644 src/kernel/types/browsers/browser_interaction_key_event.py create mode 100644 src/kernel/types/browsers/browser_interaction_scroll_settled_event.py create mode 100644 src/kernel/types/browsers/browser_monitor_disconnected_event.py create mode 100644 src/kernel/types/browsers/browser_monitor_init_failed_event.py create mode 100644 src/kernel/types/browsers/browser_monitor_reconnect_failed_event.py create mode 100644 src/kernel/types/browsers/browser_monitor_reconnected_event.py create mode 100644 src/kernel/types/browsers/browser_monitor_screenshot_event.py create mode 100644 src/kernel/types/browsers/browser_network_idle_event.py create mode 100644 src/kernel/types/browsers/browser_network_loading_failed_event.py create mode 100644 src/kernel/types/browsers/browser_network_request_event.py create mode 100644 src/kernel/types/browsers/browser_network_response_event.py create mode 100644 src/kernel/types/browsers/browser_page_dom_content_loaded_event.py create mode 100644 src/kernel/types/browsers/browser_page_layout_settled_event.py create mode 100644 src/kernel/types/browsers/browser_page_layout_shift_event.py create mode 100644 src/kernel/types/browsers/browser_page_lcp_event.py create mode 100644 src/kernel/types/browsers/browser_page_load_event.py create mode 100644 src/kernel/types/browsers/browser_page_navigation_event.py create mode 100644 src/kernel/types/browsers/browser_page_navigation_settled_event.py create mode 100644 src/kernel/types/browsers/browser_page_tab_opened_event.py create mode 100644 src/kernel/types/browsers/browser_telemetry_categories_config.py create mode 100644 src/kernel/types/browsers/browser_telemetry_categories_config_param.py create mode 100644 src/kernel/types/browsers/browser_telemetry_category_config.py create mode 100644 src/kernel/types/browsers/browser_telemetry_category_config_param.py create mode 100644 src/kernel/types/browsers/browser_telemetry_config.py create mode 100644 src/kernel/types/browsers/browser_telemetry_config_param.py create mode 100644 src/kernel/types/browsers/browser_telemetry_event.py create mode 100644 src/kernel/types/browsers/telemetry_stream_response.py create mode 100644 tests/api_resources/browsers/test_telemetry.py diff --git a/.stats.yml b/.stats.yml index 542eaa0d..3dfe6c63 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e564e74a7aae1744d4aa070a63c387f456c4719a48747dc6229b58a986255b65.yml -openapi_spec_hash: 62beb1f20708652aaee31bbffb6cfbe9 -config_hash: 08d55086449943a8fec212b870061a3f +configured_endpoints: 113 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-a9c7e806132001dbfbd4e8ae6c7d0935e503f457d63385fc800c862e3d064375.yml +openapi_spec_hash: b048dcb0c5401bc0a301c3d30cb8ecba +config_hash: 37661d89120558d34b6cc184292632f2 diff --git a/api.md b/api.md index 3dea16a3..d0b1d720 100644 --- a/api.md +++ b/api.md @@ -103,6 +103,50 @@ Methods: - client.browsers.delete_by_id(id) -> None - client.browsers.load_extensions(id, \*\*params) -> None +## Telemetry + +Types: + +```python +from kernel.types.browsers import ( + BrowserCallStack, + BrowserConsoleErrorEvent, + BrowserConsoleLogEvent, + BrowserEventContext, + BrowserEventSource, + BrowserHTTPHeaders, + BrowserInteractionClickEvent, + BrowserInteractionKeyEvent, + BrowserInteractionScrollSettledEvent, + BrowserMonitorDisconnectedEvent, + BrowserMonitorInitFailedEvent, + BrowserMonitorReconnectFailedEvent, + BrowserMonitorReconnectedEvent, + BrowserMonitorScreenshotEvent, + BrowserNetworkIdleEvent, + BrowserNetworkLoadingFailedEvent, + BrowserNetworkRequestEvent, + BrowserNetworkResponseEvent, + BrowserPageDomContentLoadedEvent, + BrowserPageLayoutSettledEvent, + BrowserPageLayoutShiftEvent, + BrowserPageLcpEvent, + BrowserPageLoadEvent, + BrowserPageNavigationEvent, + BrowserPageNavigationSettledEvent, + BrowserPageTabOpenedEvent, + BrowserTelemetryCategoriesConfig, + BrowserTelemetryCategoryConfig, + BrowserTelemetryConfig, + BrowserTelemetryEvent, + TelemetryStreamResponse, +) +``` + +Methods: + +- client.browsers.telemetry.stream(id) -> TelemetryStreamResponse + ## Replays Types: diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py index 682b2936..db8ebbeb 100644 --- a/src/kernel/resources/browser_pools.py +++ b/src/kernel/resources/browser_pools.py @@ -110,9 +110,11 @@ def create( proxy_id: Optional proxy to associate to the browser session. Must reference a proxy belonging to the caller's org. - start_url: Optional URL to open when a browser is created for the pool. Navigation is - best-effort, so navigation failures do not prevent the pool from filling. Reused - browsers keep the page left by the previous lease. + start_url: Optional URL to navigate to when a new browser is warmed into the pool. + Best-effort: failures to navigate do not fail pool fill. Only applied to + newly-warmed browsers; browsers reused via release/acquire keep whatever URL the + previous lease left them on. Accepts any URL Chromium can resolve, including + chrome:// pages. stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. @@ -259,9 +261,11 @@ def update( proxy_id: Optional proxy to associate to the browser session. Must reference a proxy belonging to the caller's org. - start_url: Optional URL to open when a browser is created for the pool. Navigation is - best-effort, so navigation failures do not prevent the pool from filling. Reused - browsers keep the page left by the previous lease. + start_url: Optional URL to navigate to when a new browser is warmed into the pool. + Best-effort: failures to navigate do not fail pool fill. Only applied to + newly-warmed browsers; browsers reused via release/acquire keep whatever URL the + previous lease left them on. Accepts any URL Chromium can resolve, including + chrome:// pages. stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. @@ -584,9 +588,11 @@ async def create( proxy_id: Optional proxy to associate to the browser session. Must reference a proxy belonging to the caller's org. - start_url: Optional URL to open when a browser is created for the pool. Navigation is - best-effort, so navigation failures do not prevent the pool from filling. Reused - browsers keep the page left by the previous lease. + start_url: Optional URL to navigate to when a new browser is warmed into the pool. + Best-effort: failures to navigate do not fail pool fill. Only applied to + newly-warmed browsers; browsers reused via release/acquire keep whatever URL the + previous lease left them on. Accepts any URL Chromium can resolve, including + chrome:// pages. stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. @@ -733,9 +739,11 @@ async def update( proxy_id: Optional proxy to associate to the browser session. Must reference a proxy belonging to the caller's org. - start_url: Optional URL to open when a browser is created for the pool. Navigation is - best-effort, so navigation failures do not prevent the pool from filling. Reused - browsers keep the page left by the previous lease. + start_url: Optional URL to navigate to when a new browser is warmed into the pool. + Best-effort: failures to navigate do not fail pool fill. Only applied to + newly-warmed browsers; browsers reused via release/acquire keep whatever URL the + previous lease left them on. Accepts any URL Chromium can resolve, including + chrome:// pages. stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. diff --git a/src/kernel/resources/browsers/__init__.py b/src/kernel/resources/browsers/__init__.py index a1acee20..928d4553 100644 --- a/src/kernel/resources/browsers/__init__.py +++ b/src/kernel/resources/browsers/__init__.py @@ -48,6 +48,14 @@ ComputerResourceWithStreamingResponse, AsyncComputerResourceWithStreamingResponse, ) +from .telemetry import ( + TelemetryResource, + AsyncTelemetryResource, + TelemetryResourceWithRawResponse, + AsyncTelemetryResourceWithRawResponse, + TelemetryResourceWithStreamingResponse, + AsyncTelemetryResourceWithStreamingResponse, +) from .playwright import ( PlaywrightResource, AsyncPlaywrightResource, @@ -58,6 +66,12 @@ ) __all__ = [ + "TelemetryResource", + "AsyncTelemetryResource", + "TelemetryResourceWithRawResponse", + "AsyncTelemetryResourceWithRawResponse", + "TelemetryResourceWithStreamingResponse", + "AsyncTelemetryResourceWithStreamingResponse", "ReplaysResource", "AsyncReplaysResource", "ReplaysResourceWithRawResponse", diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 9a8544ce..54010ea6 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -61,6 +61,14 @@ AsyncComputerResourceWithStreamingResponse, ) from ..._compat import cached_property +from .telemetry import ( + TelemetryResource, + AsyncTelemetryResource, + TelemetryResourceWithRawResponse, + AsyncTelemetryResourceWithRawResponse, + TelemetryResourceWithStreamingResponse, + AsyncTelemetryResourceWithStreamingResponse, +) from .playwright import ( PlaywrightResource, AsyncPlaywrightResource, @@ -87,6 +95,7 @@ from ...types.shared_params.browser_profile import BrowserProfile from ...types.shared_params.browser_viewport import BrowserViewport from ...types.shared_params.browser_extension import BrowserExtension +from ...types.browsers.browser_telemetry_config_param import BrowserTelemetryConfigParam __all__ = ["BrowsersResource", "AsyncBrowsersResource"] @@ -94,6 +103,11 @@ class BrowsersResource(SyncAPIResource): """Create and manage browser sessions.""" + @cached_property + def telemetry(self) -> TelemetryResource: + """Stream live telemetry events from a browser session.""" + return TelemetryResource(self._client) + @cached_property def replays(self) -> ReplaysResource: """Record and manage browser session video replays.""" @@ -156,6 +170,7 @@ def create( proxy_id: str | Omit = omit, start_url: str | Omit = omit, stealth: bool | Omit = omit, + telemetry: Optional[BrowserTelemetryConfigParam] | Omit = omit, timeout_seconds: int | Omit = omit, viewport: BrowserViewport | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -204,6 +219,10 @@ def create( stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. + telemetry: Telemetry configuration for the browser session. If provided, telemetry capture + starts with the specified category filter when the session is created. If + omitted, no telemetry capture is started. + timeout_seconds: The number of seconds of inactivity before the browser session is terminated. Activity includes CDP connections and live view connections. Defaults to 60 seconds. Minimum allowed is 10 seconds. Maximum allowed is 259200 (72 hours). We @@ -246,6 +265,7 @@ def create( "proxy_id": proxy_id, "start_url": start_url, "stealth": stealth, + "telemetry": telemetry, "timeout_seconds": timeout_seconds, "viewport": viewport, }, @@ -306,6 +326,7 @@ def update( disable_default_proxy: bool | Omit = omit, profile: BrowserProfile | Omit = omit, proxy_id: Optional[str] | Omit = omit, + telemetry: Optional[BrowserTelemetryConfigParam] | Omit = omit, viewport: browser_update_params.Viewport | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -327,6 +348,11 @@ def update( proxy_id: ID of the proxy to use. Omit to leave unchanged, set to empty string to remove proxy. + telemetry: Telemetry configuration. Omit, set to null, or set to an empty object ({}) to + leave the existing configuration unchanged (no-op). To enable capture for all + categories using VM defaults, set browser to an empty object ({"browser": {}}). + To stop capture, set every category's enabled to false. + viewport: Viewport configuration to apply to the browser session. extra_headers: Send extra headers @@ -346,6 +372,7 @@ def update( "disable_default_proxy": disable_default_proxy, "profile": profile, "proxy_id": proxy_id, + "telemetry": telemetry, "viewport": viewport, }, browser_update_params.BrowserUpdateParams, @@ -608,6 +635,11 @@ def load_extensions( class AsyncBrowsersResource(AsyncAPIResource): """Create and manage browser sessions.""" + @cached_property + def telemetry(self) -> AsyncTelemetryResource: + """Stream live telemetry events from a browser session.""" + return AsyncTelemetryResource(self._client) + @cached_property def replays(self) -> AsyncReplaysResource: """Record and manage browser session video replays.""" @@ -670,6 +702,7 @@ async def create( proxy_id: str | Omit = omit, start_url: str | Omit = omit, stealth: bool | Omit = omit, + telemetry: Optional[BrowserTelemetryConfigParam] | Omit = omit, timeout_seconds: int | Omit = omit, viewport: BrowserViewport | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -718,6 +751,10 @@ async def create( stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. + telemetry: Telemetry configuration for the browser session. If provided, telemetry capture + starts with the specified category filter when the session is created. If + omitted, no telemetry capture is started. + timeout_seconds: The number of seconds of inactivity before the browser session is terminated. Activity includes CDP connections and live view connections. Defaults to 60 seconds. Minimum allowed is 10 seconds. Maximum allowed is 259200 (72 hours). We @@ -760,6 +797,7 @@ async def create( "proxy_id": proxy_id, "start_url": start_url, "stealth": stealth, + "telemetry": telemetry, "timeout_seconds": timeout_seconds, "viewport": viewport, }, @@ -820,6 +858,7 @@ async def update( disable_default_proxy: bool | Omit = omit, profile: BrowserProfile | Omit = omit, proxy_id: Optional[str] | Omit = omit, + telemetry: Optional[BrowserTelemetryConfigParam] | Omit = omit, viewport: browser_update_params.Viewport | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -841,6 +880,11 @@ async def update( proxy_id: ID of the proxy to use. Omit to leave unchanged, set to empty string to remove proxy. + telemetry: Telemetry configuration. Omit, set to null, or set to an empty object ({}) to + leave the existing configuration unchanged (no-op). To enable capture for all + categories using VM defaults, set browser to an empty object ({"browser": {}}). + To stop capture, set every category's enabled to false. + viewport: Viewport configuration to apply to the browser session. extra_headers: Send extra headers @@ -860,6 +904,7 @@ async def update( "disable_default_proxy": disable_default_proxy, "profile": profile, "proxy_id": proxy_id, + "telemetry": telemetry, "viewport": viewport, }, browser_update_params.BrowserUpdateParams, @@ -1152,6 +1197,11 @@ def __init__(self, browsers: BrowsersResource) -> None: browsers.load_extensions, ) + @cached_property + def telemetry(self) -> TelemetryResourceWithRawResponse: + """Stream live telemetry events from a browser session.""" + return TelemetryResourceWithRawResponse(self._browsers.telemetry) + @cached_property def replays(self) -> ReplaysResourceWithRawResponse: """Record and manage browser session video replays.""" @@ -1213,6 +1263,11 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None: browsers.load_extensions, ) + @cached_property + def telemetry(self) -> AsyncTelemetryResourceWithRawResponse: + """Stream live telemetry events from a browser session.""" + return AsyncTelemetryResourceWithRawResponse(self._browsers.telemetry) + @cached_property def replays(self) -> AsyncReplaysResourceWithRawResponse: """Record and manage browser session video replays.""" @@ -1274,6 +1329,11 @@ def __init__(self, browsers: BrowsersResource) -> None: browsers.load_extensions, ) + @cached_property + def telemetry(self) -> TelemetryResourceWithStreamingResponse: + """Stream live telemetry events from a browser session.""" + return TelemetryResourceWithStreamingResponse(self._browsers.telemetry) + @cached_property def replays(self) -> ReplaysResourceWithStreamingResponse: """Record and manage browser session video replays.""" @@ -1335,6 +1395,11 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None: browsers.load_extensions, ) + @cached_property + def telemetry(self) -> AsyncTelemetryResourceWithStreamingResponse: + """Stream live telemetry events from a browser session.""" + return AsyncTelemetryResourceWithStreamingResponse(self._browsers.telemetry) + @cached_property def replays(self) -> AsyncReplaysResourceWithStreamingResponse: """Record and manage browser session video replays.""" diff --git a/src/kernel/resources/browsers/telemetry.py b/src/kernel/resources/browsers/telemetry.py new file mode 100644 index 00000000..cb95b2d1 --- /dev/null +++ b/src/kernel/resources/browsers/telemetry.py @@ -0,0 +1,195 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from ..._utils import path_template, strip_not_given +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 ..._streaming import Stream, AsyncStream +from ..._base_client import make_request_options +from ...types.browsers.telemetry_stream_response import TelemetryStreamResponse + +__all__ = ["TelemetryResource", "AsyncTelemetryResource"] + + +class TelemetryResource(SyncAPIResource): + """Stream live telemetry events from a browser session.""" + + @cached_property + def with_raw_response(self) -> TelemetryResourceWithRawResponse: + """ + 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/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return TelemetryResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> TelemetryResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response + """ + return TelemetryResourceWithStreamingResponse(self) + + def stream( + self, + id: str, + *, + last_event_id: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Stream[TelemetryStreamResponse]: + """Streams browser telemetry events as a server-sent events (SSE) stream. + + The + stream closes when the browser session terminates. Each event frame includes an + id: field containing a monotonically increasing sequence number; pass it as + Last-Event-ID on reconnect to resume without gaps. The event: field is never + set; all frames carry JSON in the data: field. A keepalive comment frame is sent + every 15 seconds when no events arrive. Returns 404 if the browser session does + not exist. If telemetry was not enabled on the session, the stream opens but no + events are delivered. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} + extra_headers = {**strip_not_given({"Last-Event-ID": last_event_id}), **(extra_headers or {})} + return self._get( + path_template("/browsers/{id}/telemetry", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=TelemetryStreamResponse, + stream=True, + stream_cls=Stream[TelemetryStreamResponse], + ) + + +class AsyncTelemetryResource(AsyncAPIResource): + """Stream live telemetry events from a browser session.""" + + @cached_property + def with_raw_response(self) -> AsyncTelemetryResourceWithRawResponse: + """ + 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/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncTelemetryResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncTelemetryResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response + """ + return AsyncTelemetryResourceWithStreamingResponse(self) + + async def stream( + self, + id: str, + *, + last_event_id: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncStream[TelemetryStreamResponse]: + """Streams browser telemetry events as a server-sent events (SSE) stream. + + The + stream closes when the browser session terminates. Each event frame includes an + id: field containing a monotonically increasing sequence number; pass it as + Last-Event-ID on reconnect to resume without gaps. The event: field is never + set; all frames carry JSON in the data: field. A keepalive comment frame is sent + every 15 seconds when no events arrive. Returns 404 if the browser session does + not exist. If telemetry was not enabled on the session, the stream opens but no + events are delivered. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} + extra_headers = {**strip_not_given({"Last-Event-ID": last_event_id}), **(extra_headers or {})} + return await self._get( + path_template("/browsers/{id}/telemetry", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=TelemetryStreamResponse, + stream=True, + stream_cls=AsyncStream[TelemetryStreamResponse], + ) + + +class TelemetryResourceWithRawResponse: + def __init__(self, telemetry: TelemetryResource) -> None: + self._telemetry = telemetry + + self.stream = to_raw_response_wrapper( + telemetry.stream, + ) + + +class AsyncTelemetryResourceWithRawResponse: + def __init__(self, telemetry: AsyncTelemetryResource) -> None: + self._telemetry = telemetry + + self.stream = async_to_raw_response_wrapper( + telemetry.stream, + ) + + +class TelemetryResourceWithStreamingResponse: + def __init__(self, telemetry: TelemetryResource) -> None: + self._telemetry = telemetry + + self.stream = to_streamed_response_wrapper( + telemetry.stream, + ) + + +class AsyncTelemetryResourceWithStreamingResponse: + def __init__(self, telemetry: AsyncTelemetryResource) -> None: + self._telemetry = telemetry + + self.stream = async_to_streamed_response_wrapper( + telemetry.stream, + ) diff --git a/src/kernel/resources/proxies.py b/src/kernel/resources/proxies.py index bacdd570..501a2e88 100644 --- a/src/kernel/resources/proxies.py +++ b/src/kernel/resources/proxies.py @@ -222,7 +222,9 @@ def check( between requests, so a successful check validates proxy configuration but does not guarantee that a subsequent browser session will use the same exit IP or reach the same site — it is useful for verifying credentials and connectivity, - not for predicting site-specific behavior. + not for predicting site-specific behavior. When provided, the check result does + not update the proxy's health status, since a failure may indicate a problem + with the target site rather than the proxy itself. extra_headers: Send extra headers @@ -440,7 +442,9 @@ async def check( between requests, so a successful check validates proxy configuration but does not guarantee that a subsequent browser session will use the same exit IP or reach the same site — it is useful for verifying credentials and connectivity, - not for predicting site-specific behavior. + not for predicting site-specific behavior. When provided, the check result does + not update the proxy's health status, since a failure may indicate a problem + with the target site rather than the proxy itself. extra_headers: Send extra headers diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 3838047e..1a7d19c3 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -2,6 +2,8 @@ from __future__ import annotations +from . import browsers +from .. import _compat from .shared import ( LogEvent as LogEvent, AppAction as AppAction, @@ -91,3 +93,18 @@ from .extension_download_from_chrome_store_params import ( ExtensionDownloadFromChromeStoreParams as ExtensionDownloadFromChromeStoreParams, ) + +# Rebuild cyclical models only after all modules are imported. +# This ensures that, when building the deferred (due to cyclical references) model schema, +# Pydantic can resolve the necessary references. +# See: https://github.com/pydantic/pydantic/issues/11250 for more context. +if _compat.PYDANTIC_V1: + browsers.browser_call_stack.BrowserCallStack.update_forward_refs() # type: ignore + browsers.browser_console_error_event.BrowserConsoleErrorEvent.update_forward_refs() # type: ignore + browsers.browser_console_log_event.BrowserConsoleLogEvent.update_forward_refs() # type: ignore + browsers.telemetry_stream_response.TelemetryStreamResponse.update_forward_refs() # type: ignore +else: + browsers.browser_call_stack.BrowserCallStack.model_rebuild(_parent_namespace_depth=0) + browsers.browser_console_error_event.BrowserConsoleErrorEvent.model_rebuild(_parent_namespace_depth=0) + browsers.browser_console_log_event.BrowserConsoleLogEvent.model_rebuild(_parent_namespace_depth=0) + browsers.telemetry_stream_response.TelemetryStreamResponse.model_rebuild(_parent_namespace_depth=0) diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index c05fd94e..ca184fc7 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -2,13 +2,14 @@ from __future__ import annotations -from typing import Dict, Iterable +from typing import Dict, Iterable, Optional from typing_extensions import TypedDict from .browser_persistence_param import BrowserPersistenceParam from .shared_params.browser_profile import BrowserProfile from .shared_params.browser_viewport import BrowserViewport from .shared_params.browser_extension import BrowserExtension +from .browsers.browser_telemetry_config_param import BrowserTelemetryConfigParam __all__ = ["BrowserCreateParams"] @@ -79,6 +80,13 @@ class BrowserCreateParams(TypedDict, total=False): mechanisms. """ + telemetry: Optional[BrowserTelemetryConfigParam] + """Telemetry configuration for the browser session. + + If provided, telemetry capture starts with the specified category filter when + the session is created. If omitted, no telemetry capture is started. + """ + timeout_seconds: int """The number of seconds of inactivity before the browser session is terminated. diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index 2e2e5dbc..bbb2287e 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -9,6 +9,7 @@ from .browser_pool_ref import BrowserPoolRef from .browser_persistence import BrowserPersistence from .shared.browser_viewport import BrowserViewport +from .browsers.browser_telemetry_config import BrowserTelemetryConfig __all__ = ["BrowserCreateResponse"] @@ -76,7 +77,16 @@ class BrowserCreateResponse(BaseModel): """ID of the proxy associated with this browser session, if any.""" start_url: Optional[str] = None - """Start URL requested for the session, if provided.""" + """URL the session was asked to navigate to on creation, if any. + + Recorded for debugging. Navigation is fire-and-forget — the URL is dispatched to + the browser without waiting for it to load, and any errors (DNS failure, bad + status, timeout) are silently dropped. Captures what was requested, not what the + browser actually loaded. + """ + + telemetry: Optional[BrowserTelemetryConfig] = None + """Active telemetry configuration for the session, if any.""" usage: Optional[BrowserUsage] = None """Session usage metrics.""" diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index 27e409f4..ed14251a 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -9,6 +9,7 @@ from .browser_pool_ref import BrowserPoolRef from .browser_persistence import BrowserPersistence from .shared.browser_viewport import BrowserViewport +from .browsers.browser_telemetry_config import BrowserTelemetryConfig __all__ = ["BrowserListResponse"] @@ -76,7 +77,16 @@ class BrowserListResponse(BaseModel): """ID of the proxy associated with this browser session, if any.""" start_url: Optional[str] = None - """Start URL requested for the session, if provided.""" + """URL the session was asked to navigate to on creation, if any. + + Recorded for debugging. Navigation is fire-and-forget — the URL is dispatched to + the browser without waiting for it to load, and any errors (DNS failure, bad + status, timeout) are silently dropped. Captures what was requested, not what the + browser actually loaded. + """ + + telemetry: Optional[BrowserTelemetryConfig] = None + """Active telemetry configuration for the session, if any.""" usage: Optional[BrowserUsage] = None """Session usage metrics.""" diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py index e456fca2..8ead6807 100644 --- a/src/kernel/types/browser_pool.py +++ b/src/kernel/types/browser_pool.py @@ -64,10 +64,12 @@ class BrowserPoolConfig(BaseModel): """ start_url: Optional[str] = None - """Optional URL to open when a browser is created for the pool. + """Optional URL to navigate to when a new browser is warmed into the pool. - Navigation is best-effort, so navigation failures do not prevent the pool from - filling. Reused browsers keep the page left by the previous lease. + Best-effort: failures to navigate do not fail pool fill. Only applied to + newly-warmed browsers; browsers reused via release/acquire keep whatever URL the + previous lease left them on. Accepts any URL Chromium can resolve, including + chrome:// pages. """ stealth: Optional[bool] = None diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py index 9cf914ee..20ab1fc5 100644 --- a/src/kernel/types/browser_pool_acquire_response.py +++ b/src/kernel/types/browser_pool_acquire_response.py @@ -9,6 +9,7 @@ from .browser_pool_ref import BrowserPoolRef from .browser_persistence import BrowserPersistence from .shared.browser_viewport import BrowserViewport +from .browsers.browser_telemetry_config import BrowserTelemetryConfig __all__ = ["BrowserPoolAcquireResponse"] @@ -76,7 +77,16 @@ class BrowserPoolAcquireResponse(BaseModel): """ID of the proxy associated with this browser session, if any.""" start_url: Optional[str] = None - """Start URL requested for the session, if provided.""" + """URL the session was asked to navigate to on creation, if any. + + Recorded for debugging. Navigation is fire-and-forget — the URL is dispatched to + the browser without waiting for it to load, and any errors (DNS failure, bad + status, timeout) are silently dropped. Captures what was requested, not what the + browser actually loaded. + """ + + telemetry: Optional[BrowserTelemetryConfig] = None + """Active telemetry configuration for the session, if any.""" usage: Optional[BrowserUsage] = None """Session usage metrics.""" diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py index bc08c3f2..1ffeeb75 100644 --- a/src/kernel/types/browser_pool_create_params.py +++ b/src/kernel/types/browser_pool_create_params.py @@ -63,10 +63,12 @@ class BrowserPoolCreateParams(TypedDict, total=False): """ start_url: str - """Optional URL to open when a browser is created for the pool. + """Optional URL to navigate to when a new browser is warmed into the pool. - Navigation is best-effort, so navigation failures do not prevent the pool from - filling. Reused browsers keep the page left by the previous lease. + Best-effort: failures to navigate do not fail pool fill. Only applied to + newly-warmed browsers; browsers reused via release/acquire keep whatever URL the + previous lease left them on. Accepts any URL Chromium can resolve, including + chrome:// pages. """ stealth: bool diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py index 043eaf85..4efc9ced 100644 --- a/src/kernel/types/browser_pool_update_params.py +++ b/src/kernel/types/browser_pool_update_params.py @@ -69,10 +69,12 @@ class BrowserPoolUpdateParams(TypedDict, total=False): """ start_url: str - """Optional URL to open when a browser is created for the pool. + """Optional URL to navigate to when a new browser is warmed into the pool. - Navigation is best-effort, so navigation failures do not prevent the pool from - filling. Reused browsers keep the page left by the previous lease. + Best-effort: failures to navigate do not fail pool fill. Only applied to + newly-warmed browsers; browsers reused via release/acquire keep whatever URL the + previous lease left them on. Accepts any URL Chromium can resolve, including + chrome:// pages. """ stealth: bool diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 95cfa9ad..6c79b519 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -9,6 +9,7 @@ from .browser_pool_ref import BrowserPoolRef from .browser_persistence import BrowserPersistence from .shared.browser_viewport import BrowserViewport +from .browsers.browser_telemetry_config import BrowserTelemetryConfig __all__ = ["BrowserRetrieveResponse"] @@ -76,7 +77,16 @@ class BrowserRetrieveResponse(BaseModel): """ID of the proxy associated with this browser session, if any.""" start_url: Optional[str] = None - """Start URL requested for the session, if provided.""" + """URL the session was asked to navigate to on creation, if any. + + Recorded for debugging. Navigation is fire-and-forget — the URL is dispatched to + the browser without waiting for it to load, and any errors (DNS failure, bad + status, timeout) are silently dropped. Captures what was requested, not what the + browser actually loaded. + """ + + telemetry: Optional[BrowserTelemetryConfig] = None + """Active telemetry configuration for the session, if any.""" usage: Optional[BrowserUsage] = None """Session usage metrics.""" diff --git a/src/kernel/types/browser_update_params.py b/src/kernel/types/browser_update_params.py index e0f7588e..297d0388 100644 --- a/src/kernel/types/browser_update_params.py +++ b/src/kernel/types/browser_update_params.py @@ -7,6 +7,7 @@ from .shared_params.browser_profile import BrowserProfile from .shared_params.browser_viewport import BrowserViewport +from .browsers.browser_telemetry_config_param import BrowserTelemetryConfigParam __all__ = ["BrowserUpdateParams", "Viewport"] @@ -30,6 +31,15 @@ class BrowserUpdateParams(TypedDict, total=False): Omit to leave unchanged, set to empty string to remove proxy. """ + telemetry: Optional[BrowserTelemetryConfigParam] + """Telemetry configuration. + + Omit, set to null, or set to an empty object ({}) to leave the existing + configuration unchanged (no-op). To enable capture for all categories using VM + defaults, set browser to an empty object ({"browser": {}}). To stop capture, set + every category's enabled to false. + """ + viewport: Viewport """Viewport configuration to apply to the browser session.""" diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py index 209d58c4..4f237cf5 100644 --- a/src/kernel/types/browser_update_response.py +++ b/src/kernel/types/browser_update_response.py @@ -9,6 +9,7 @@ from .browser_pool_ref import BrowserPoolRef from .browser_persistence import BrowserPersistence from .shared.browser_viewport import BrowserViewport +from .browsers.browser_telemetry_config import BrowserTelemetryConfig __all__ = ["BrowserUpdateResponse"] @@ -76,7 +77,16 @@ class BrowserUpdateResponse(BaseModel): """ID of the proxy associated with this browser session, if any.""" start_url: Optional[str] = None - """Start URL requested for the session, if provided.""" + """URL the session was asked to navigate to on creation, if any. + + Recorded for debugging. Navigation is fire-and-forget — the URL is dispatched to + the browser without waiting for it to load, and any errors (DNS failure, bad + status, timeout) are silently dropped. Captures what was requested, not what the + browser actually loaded. + """ + + telemetry: Optional[BrowserTelemetryConfig] = None + """Active telemetry configuration for the session, if any.""" usage: Optional[BrowserUsage] = None """Session usage metrics.""" diff --git a/src/kernel/types/browsers/__init__.py b/src/kernel/types/browsers/__init__.py index 1e47205d..a3bca412 100644 --- a/src/kernel/types/browsers/__init__.py +++ b/src/kernel/types/browsers/__init__.py @@ -5,6 +5,7 @@ from .f_move_params import FMoveParams as FMoveParams from .f_upload_params import FUploadParams as FUploadParams from .log_stream_params import LogStreamParams as LogStreamParams +from .browser_call_stack import BrowserCallStack as BrowserCallStack from .f_file_info_params import FFileInfoParams as FFileInfoParams from .f_read_file_params import FReadFileParams as FReadFileParams from .f_list_files_params import FListFilesParams as FListFilesParams @@ -13,41 +14,84 @@ from .process_exec_params import ProcessExecParams as ProcessExecParams from .process_kill_params import ProcessKillParams as ProcessKillParams from .replay_start_params import ReplayStartParams as ReplayStartParams +from .browser_event_source import BrowserEventSource as BrowserEventSource +from .browser_http_headers import BrowserHTTPHeaders as BrowserHTTPHeaders from .f_delete_file_params import FDeleteFileParams as FDeleteFileParams from .f_file_info_response import FFileInfoResponse as FFileInfoResponse from .process_spawn_params import ProcessSpawnParams as ProcessSpawnParams from .process_stdin_params import ProcessStdinParams as ProcessStdinParams from .replay_list_response import ReplayListResponse as ReplayListResponse +from .browser_event_context import BrowserEventContext as BrowserEventContext from .computer_batch_params import ComputerBatchParams as ComputerBatchParams from .f_list_files_response import FListFilesResponse as FListFilesResponse from .process_exec_response import ProcessExecResponse as ProcessExecResponse from .process_kill_response import ProcessKillResponse as ProcessKillResponse from .process_resize_params import ProcessResizeParams as ProcessResizeParams from .replay_start_response import ReplayStartResponse as ReplayStartResponse +from .browser_page_lcp_event import BrowserPageLcpEvent as BrowserPageLcpEvent from .computer_scroll_params import ComputerScrollParams as ComputerScrollParams from .process_spawn_response import ProcessSpawnResponse as ProcessSpawnResponse from .process_stdin_response import ProcessStdinResponse as ProcessStdinResponse +from .browser_page_load_event import BrowserPageLoadEvent as BrowserPageLoadEvent +from .browser_telemetry_event import BrowserTelemetryEvent as BrowserTelemetryEvent from .process_resize_response import ProcessResizeResponse as ProcessResizeResponse from .process_status_response import ProcessStatusResponse as ProcessStatusResponse +from .browser_telemetry_config import BrowserTelemetryConfig as BrowserTelemetryConfig +from .browser_console_log_event import BrowserConsoleLogEvent as BrowserConsoleLogEvent from .computer_press_key_params import ComputerPressKeyParams as ComputerPressKeyParams from .computer_type_text_params import ComputerTypeTextParams as ComputerTypeTextParams from .f_create_directory_params import FCreateDirectoryParams as FCreateDirectoryParams from .f_delete_directory_params import FDeleteDirectoryParams as FDeleteDirectoryParams from .f_download_dir_zip_params import FDownloadDirZipParams as FDownloadDirZipParams from .playwright_execute_params import PlaywrightExecuteParams as PlaywrightExecuteParams +from .telemetry_stream_response import TelemetryStreamResponse as TelemetryStreamResponse +from .browser_network_idle_event import BrowserNetworkIdleEvent as BrowserNetworkIdleEvent from .computer_drag_mouse_params import ComputerDragMouseParams as ComputerDragMouseParams from .computer_move_mouse_params import ComputerMoveMouseParams as ComputerMoveMouseParams +from .browser_console_error_event import BrowserConsoleErrorEvent as BrowserConsoleErrorEvent from .computer_click_mouse_params import ComputerClickMouseParams as ComputerClickMouseParams from .playwright_execute_response import PlaywrightExecuteResponse as PlaywrightExecuteResponse +from .browser_interaction_key_event import BrowserInteractionKeyEvent as BrowserInteractionKeyEvent +from .browser_network_request_event import BrowserNetworkRequestEvent as BrowserNetworkRequestEvent +from .browser_page_navigation_event import BrowserPageNavigationEvent as BrowserPageNavigationEvent +from .browser_page_tab_opened_event import BrowserPageTabOpenedEvent as BrowserPageTabOpenedEvent from .f_set_file_permissions_params import FSetFilePermissionsParams as FSetFilePermissionsParams +from .browser_network_response_event import BrowserNetworkResponseEvent as BrowserNetworkResponseEvent +from .browser_telemetry_config_param import BrowserTelemetryConfigParam as BrowserTelemetryConfigParam from .process_stdout_stream_response import ProcessStdoutStreamResponse as ProcessStdoutStreamResponse +from .browser_interaction_click_event import BrowserInteractionClickEvent as BrowserInteractionClickEvent +from .browser_page_layout_shift_event import BrowserPageLayoutShiftEvent as BrowserPageLayoutShiftEvent from .computer_write_clipboard_params import ComputerWriteClipboardParams as ComputerWriteClipboardParams +from .browser_monitor_screenshot_event import BrowserMonitorScreenshotEvent as BrowserMonitorScreenshotEvent from .computer_read_clipboard_response import ComputerReadClipboardResponse as ComputerReadClipboardResponse +from .browser_monitor_init_failed_event import BrowserMonitorInitFailedEvent as BrowserMonitorInitFailedEvent +from .browser_monitor_reconnected_event import BrowserMonitorReconnectedEvent as BrowserMonitorReconnectedEvent +from .browser_page_layout_settled_event import BrowserPageLayoutSettledEvent as BrowserPageLayoutSettledEvent +from .browser_telemetry_category_config import BrowserTelemetryCategoryConfig as BrowserTelemetryCategoryConfig +from .browser_monitor_disconnected_event import BrowserMonitorDisconnectedEvent as BrowserMonitorDisconnectedEvent from .computer_capture_screenshot_params import ComputerCaptureScreenshotParams as ComputerCaptureScreenshotParams +from .browser_telemetry_categories_config import BrowserTelemetryCategoriesConfig as BrowserTelemetryCategoriesConfig +from .browser_network_loading_failed_event import BrowserNetworkLoadingFailedEvent as BrowserNetworkLoadingFailedEvent from .computer_get_mouse_position_response import ComputerGetMousePositionResponse as ComputerGetMousePositionResponse +from .browser_page_dom_content_loaded_event import BrowserPageDomContentLoadedEvent as BrowserPageDomContentLoadedEvent +from .browser_page_navigation_settled_event import ( + BrowserPageNavigationSettledEvent as BrowserPageNavigationSettledEvent, +) from .computer_set_cursor_visibility_params import ( ComputerSetCursorVisibilityParams as ComputerSetCursorVisibilityParams, ) +from .browser_monitor_reconnect_failed_event import ( + BrowserMonitorReconnectFailedEvent as BrowserMonitorReconnectFailedEvent, +) +from .browser_telemetry_category_config_param import ( + BrowserTelemetryCategoryConfigParam as BrowserTelemetryCategoryConfigParam, +) from .computer_set_cursor_visibility_response import ( ComputerSetCursorVisibilityResponse as ComputerSetCursorVisibilityResponse, ) +from .browser_interaction_scroll_settled_event import ( + BrowserInteractionScrollSettledEvent as BrowserInteractionScrollSettledEvent, +) +from .browser_telemetry_categories_config_param import ( + BrowserTelemetryCategoriesConfigParam as BrowserTelemetryCategoriesConfigParam, +) diff --git a/src/kernel/types/browsers/browser_call_stack.py b/src/kernel/types/browsers/browser_call_stack.py new file mode 100644 index 00000000..d9e97ffd --- /dev/null +++ b/src/kernel/types/browsers/browser_call_stack.py @@ -0,0 +1,43 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Optional + +from pydantic import Field as FieldInfo + +from ..._models import BaseModel + +__all__ = ["BrowserCallStack", "CallFrame"] + + +class CallFrame(BaseModel): + column_number: int = FieldInfo(alias="columnNumber") + """Zero-based column number within the line.""" + + function_name: str = FieldInfo(alias="functionName") + """JavaScript function name, or empty string for anonymous functions.""" + + line_number: int = FieldInfo(alias="lineNumber") + """Zero-based line number within the script.""" + + script_id: str = FieldInfo(alias="scriptId") + """CDP script identifier.""" + + url: str + """URL or name of the script file.""" + + +class BrowserCallStack(BaseModel): + """ + CDP Runtime.StackTrace representing the JavaScript call stack at the time of an event. Fields use CDP naming conventions rather than snake_case to match the Chrome DevTools Protocol wire format. + """ + + call_frames: List[CallFrame] = FieldInfo(alias="callFrames") + """Ordered list of call frames, outermost first.""" + + description: Optional[str] = None + """Optional label for the stack trace (e.g. async cause).""" + + parent: Optional["BrowserCallStack"] = None + """Parent stack trace for async stacks.""" diff --git a/src/kernel/types/browsers/browser_console_error_event.py b/src/kernel/types/browsers/browser_console_error_event.py new file mode 100644 index 00000000..a152f944 --- /dev/null +++ b/src/kernel/types/browsers/browser_console_error_event.py @@ -0,0 +1,87 @@ +# 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 Literal + +from ..._models import BaseModel +from .browser_event_source import BrowserEventSource +from .browser_event_context import BrowserEventContext + +__all__ = ["BrowserConsoleErrorEvent", "Data"] + + +class Data(BrowserEventContext): + """Browser event context stamped by the browser monitor onto all CDP-sourced events. + + Identifies the target, frame, and navigation epoch in which the event occurred. + """ + + text: str + """Error message text. Present in both source paths.""" + + args: Optional[List[str]] = None + """All console arguments coerced to strings. + + Present only when sourced from Runtime.consoleAPICalled. + """ + + column: Optional[int] = None + """Column number in the script where the exception was thrown. + + Present only when sourced from Runtime.exceptionThrown. + """ + + level: Optional[str] = None + """CDP console type value, always "error". + + Present only when sourced from Runtime.consoleAPICalled. + """ + + line: Optional[int] = None + """Line number in the script where the exception was thrown. + + Present only when sourced from Runtime.exceptionThrown. + """ + + source_url: Optional[str] = None + """URL of the script file that threw the exception. + + Present only when sourced from Runtime.exceptionThrown. + """ + + stack_trace: Optional["BrowserCallStack"] = None + """ + CDP Runtime.StackTrace representing the JavaScript call stack at the time of an + event. Fields use CDP naming conventions rather than snake_case to match the + Chrome DevTools Protocol wire format. + """ + + +class BrowserConsoleErrorEvent(BaseModel): + """A browser console error or uncaught JavaScript exception event. + + Emitted from two distinct CDP sources with different data shapes. Runtime.consoleAPICalled (console.error calls) produces level, text, args, and stack_trace. Runtime.exceptionThrown (uncaught exceptions) produces text, line, column, source_url, and stack_trace. Fields not applicable to the source are absent. + """ + + source: BrowserEventSource + """Provenance metadata identifying which producer emitted the event.""" + + ts: int + """Event timestamp in Unix microseconds.""" + + type: Literal["console_error"] + + data: Optional[Data] = None + """Browser event context stamped by the browser monitor onto all CDP-sourced + events. + + Identifies the target, frame, and navigation epoch in which the event occurred. + """ + + truncated: Optional[bool] = None + """True if the data field was truncated due to size limits.""" + + +from .browser_call_stack import BrowserCallStack diff --git a/src/kernel/types/browsers/browser_console_log_event.py b/src/kernel/types/browsers/browser_console_log_event.py new file mode 100644 index 00000000..5994c0be --- /dev/null +++ b/src/kernel/types/browsers/browser_console_log_event.py @@ -0,0 +1,64 @@ +# 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 Literal + +from ..._models import BaseModel +from .browser_event_source import BrowserEventSource +from .browser_event_context import BrowserEventContext + +__all__ = ["BrowserConsoleLogEvent", "Data"] + + +class Data(BrowserEventContext): + """Browser event context stamped by the browser monitor onto all CDP-sourced events. + + Identifies the target, frame, and navigation epoch in which the event occurred. + """ + + args: Optional[List[str]] = None + """All console arguments coerced to strings.""" + + level: Optional[str] = None + """CDP Runtime.consoleAPICalled type, passed through unfiltered from Chrome. + + error is routed to console_error events instead; all other CDP console types + appear here. See CDP spec for the full enum. + """ + + stack_trace: Optional["BrowserCallStack"] = None + """ + CDP Runtime.StackTrace representing the JavaScript call stack at the time of an + event. Fields use CDP naming conventions rather than snake_case to match the + Chrome DevTools Protocol wire format. + """ + + text: Optional[str] = None + """First console argument coerced to string.""" + + +class BrowserConsoleLogEvent(BaseModel): + """A browser console log event (console.log, console.info, console.warn, etc.).""" + + source: BrowserEventSource + """Provenance metadata identifying which producer emitted the event.""" + + ts: int + """Event timestamp in Unix microseconds.""" + + type: Literal["console_log"] + + data: Optional[Data] = None + """Browser event context stamped by the browser monitor onto all CDP-sourced + events. + + Identifies the target, frame, and navigation epoch in which the event occurred. + """ + + truncated: Optional[bool] = None + """True if the data field was truncated due to size limits.""" + + +from .browser_call_stack import BrowserCallStack diff --git a/src/kernel/types/browsers/browser_event_context.py b/src/kernel/types/browsers/browser_event_context.py new file mode 100644 index 00000000..6f2e9549 --- /dev/null +++ b/src/kernel/types/browsers/browser_event_context.py @@ -0,0 +1,42 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["BrowserEventContext"] + + +class BrowserEventContext(BaseModel): + """Browser event context stamped by the browser monitor onto all CDP-sourced events. + + Identifies the target, frame, and navigation epoch in which the event occurred. + """ + + frame_id: Optional[str] = None + """CDP frame identifier within the target.""" + + loader_id: Optional[str] = None + """CDP document loader identifier, reset on each navigation.""" + + nav_seq: Optional[int] = None + """ + Monotonically increasing navigation sequence number, incremented on each + top-level navigation within the target. + """ + + session_id: Optional[str] = None + """CDP session identifier for the target connection.""" + + target_id: Optional[str] = None + """Browser target identifier (stable across navigations within a tab).""" + + target_type: Optional[Literal["page", "background_page", "service_worker", "shared_worker", "other"]] = None + """CDP target type of the page that produced the event.""" + + url: Optional[str] = None + """ + URL relevant to this event — page URL for navigation and page events, request + URL for network events. + """ diff --git a/src/kernel/types/browsers/browser_event_source.py b/src/kernel/types/browsers/browser_event_source.py new file mode 100644 index 00000000..40125b6b --- /dev/null +++ b/src/kernel/types/browsers/browser_event_source.py @@ -0,0 +1,30 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, Optional +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["BrowserEventSource"] + + +class BrowserEventSource(BaseModel): + """Provenance metadata identifying which producer emitted the event.""" + + kind: Literal["cdp", "kernel_api", "extension", "local_process"] + """Event producer. + + cdp: Chrome DevTools Protocol events from the browser. kernel_api: Kernel API + server. extension: injected Chrome extension. local_process: system process + running alongside the browser. + """ + + event: Optional[str] = None + """Producer-specific event name (e.g. + + Runtime.consoleAPICalled for CDP-sourced console events, Runtime.exceptionThrown + for uncaught exceptions). + """ + + metadata: Optional[Dict[str, str]] = None + """Producer-specific context (e.g. CDP target/session/frame IDs).""" diff --git a/src/kernel/types/browsers/browser_http_headers.py b/src/kernel/types/browsers/browser_http_headers.py new file mode 100644 index 00000000..b55ae855 --- /dev/null +++ b/src/kernel/types/browsers/browser_http_headers.py @@ -0,0 +1,8 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict +from typing_extensions import TypeAlias + +__all__ = ["BrowserHTTPHeaders"] + +BrowserHTTPHeaders: TypeAlias = Dict[str, object] diff --git a/src/kernel/types/browsers/browser_interaction_click_event.py b/src/kernel/types/browsers/browser_interaction_click_event.py new file mode 100644 index 00000000..efc76b34 --- /dev/null +++ b/src/kernel/types/browsers/browser_interaction_click_event.py @@ -0,0 +1,54 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from .browser_event_source import BrowserEventSource +from .browser_event_context import BrowserEventContext + +__all__ = ["BrowserInteractionClickEvent", "Data"] + + +class Data(BrowserEventContext): + """Browser event context stamped by the browser monitor onto all CDP-sourced events. + + Identifies the target, frame, and navigation epoch in which the event occurred. + """ + + selector: Optional[str] = None + """CSS selector path to the clicked element.""" + + tag: Optional[str] = None + """HTML tag name of the clicked element in uppercase (e.g. BUTTON, A, DIV).""" + + text: Optional[str] = None + """Visible text content of the clicked element, trimmed.""" + + x: Optional[int] = None + """Viewport x-coordinate of the click in CSS pixels.""" + + y: Optional[int] = None + """Viewport y-coordinate of the click in CSS pixels.""" + + +class BrowserInteractionClickEvent(BaseModel): + """A browser user click event captured via injected page script.""" + + source: BrowserEventSource + """Provenance metadata identifying which producer emitted the event.""" + + ts: int + """Event timestamp in Unix microseconds.""" + + type: Literal["interaction_click"] + + data: Optional[Data] = None + """Browser event context stamped by the browser monitor onto all CDP-sourced + events. + + Identifies the target, frame, and navigation epoch in which the event occurred. + """ + + truncated: Optional[bool] = None + """True if the data field was truncated due to size limits.""" diff --git a/src/kernel/types/browsers/browser_interaction_key_event.py b/src/kernel/types/browsers/browser_interaction_key_event.py new file mode 100644 index 00000000..e8860330 --- /dev/null +++ b/src/kernel/types/browsers/browser_interaction_key_event.py @@ -0,0 +1,48 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from .browser_event_source import BrowserEventSource +from .browser_event_context import BrowserEventContext + +__all__ = ["BrowserInteractionKeyEvent", "Data"] + + +class Data(BrowserEventContext): + """Browser event context stamped by the browser monitor onto all CDP-sourced events. + + Identifies the target, frame, and navigation epoch in which the event occurred. + """ + + key: Optional[str] = None + """Key value from the KeyboardEvent (e.g. Enter, Backspace, a).""" + + selector: Optional[str] = None + """CSS selector path to the element that had focus when the key was pressed.""" + + tag: Optional[str] = None + """HTML tag name of the focused element in uppercase (e.g. INPUT, TEXTAREA, DIV).""" + + +class BrowserInteractionKeyEvent(BaseModel): + """A browser keyboard event captured via injected page script.""" + + source: BrowserEventSource + """Provenance metadata identifying which producer emitted the event.""" + + ts: int + """Event timestamp in Unix microseconds.""" + + type: Literal["interaction_key"] + + data: Optional[Data] = None + """Browser event context stamped by the browser monitor onto all CDP-sourced + events. + + Identifies the target, frame, and navigation epoch in which the event occurred. + """ + + truncated: Optional[bool] = None + """True if the data field was truncated due to size limits.""" diff --git a/src/kernel/types/browsers/browser_interaction_scroll_settled_event.py b/src/kernel/types/browsers/browser_interaction_scroll_settled_event.py new file mode 100644 index 00000000..16b7536b --- /dev/null +++ b/src/kernel/types/browsers/browser_interaction_scroll_settled_event.py @@ -0,0 +1,56 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from .browser_event_source import BrowserEventSource +from .browser_event_context import BrowserEventContext + +__all__ = ["BrowserInteractionScrollSettledEvent", "Data"] + + +class Data(BrowserEventContext): + """Browser event context stamped by the browser monitor onto all CDP-sourced events. + + Identifies the target, frame, and navigation epoch in which the event occurred. + """ + + from_x: Optional[int] = None + """Scroll x-position at the start of the scroll gesture in CSS pixels.""" + + from_y: Optional[int] = None + """Scroll y-position at the start of the scroll gesture in CSS pixels.""" + + target_selector: Optional[str] = None + """CSS selector path to the scrolled element.""" + + to_x: Optional[int] = None + """Final scroll x-position after the gesture settled in CSS pixels.""" + + to_y: Optional[int] = None + """Final scroll y-position after the gesture settled in CSS pixels.""" + + +class BrowserInteractionScrollSettledEvent(BaseModel): + """ + A browser scroll settled event emitted after scroll position stops changing, captured via injected page script. + """ + + source: BrowserEventSource + """Provenance metadata identifying which producer emitted the event.""" + + ts: int + """Event timestamp in Unix microseconds.""" + + type: Literal["interaction_scroll_settled"] + + data: Optional[Data] = None + """Browser event context stamped by the browser monitor onto all CDP-sourced + events. + + Identifies the target, frame, and navigation epoch in which the event occurred. + """ + + truncated: Optional[bool] = None + """True if the data field was truncated due to size limits.""" diff --git a/src/kernel/types/browsers/browser_monitor_disconnected_event.py b/src/kernel/types/browsers/browser_monitor_disconnected_event.py new file mode 100644 index 00000000..b329ab13 --- /dev/null +++ b/src/kernel/types/browsers/browser_monitor_disconnected_event.py @@ -0,0 +1,34 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from .browser_event_source import BrowserEventSource + +__all__ = ["BrowserMonitorDisconnectedEvent", "Data"] + + +class Data(BaseModel): + reason: Optional[Literal["chrome_restarted"]] = None + """Reason for the disconnection. chrome_restarted: Chrome process restarted.""" + + +class BrowserMonitorDisconnectedEvent(BaseModel): + """The CDP connection to Chrome was lost. + + Telemetry events may be dropped until monitor_reconnected arrives. Treat any in-progress computed state (network_idle, page_layout_settled) as unreliable until then. + """ + + source: BrowserEventSource + """Provenance metadata identifying which producer emitted the event.""" + + ts: int + """Event timestamp in Unix microseconds.""" + + type: Literal["monitor_disconnected"] + + data: Optional[Data] = None + + truncated: Optional[bool] = None + """True if the data field was truncated due to size limits.""" diff --git a/src/kernel/types/browsers/browser_monitor_init_failed_event.py b/src/kernel/types/browsers/browser_monitor_init_failed_event.py new file mode 100644 index 00000000..a745fb8d --- /dev/null +++ b/src/kernel/types/browsers/browser_monitor_init_failed_event.py @@ -0,0 +1,31 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from .browser_event_source import BrowserEventSource + +__all__ = ["BrowserMonitorInitFailedEvent", "Data"] + + +class Data(BaseModel): + step: Optional[str] = None + """The CDP method or initialization step that failed (e.g. Target.setAutoAttach).""" + + +class BrowserMonitorInitFailedEvent(BaseModel): + """The CDP session could not be initialized.""" + + source: BrowserEventSource + """Provenance metadata identifying which producer emitted the event.""" + + ts: int + """Event timestamp in Unix microseconds.""" + + type: Literal["monitor_init_failed"] + + data: Optional[Data] = None + + truncated: Optional[bool] = None + """True if the data field was truncated due to size limits.""" diff --git a/src/kernel/types/browsers/browser_monitor_reconnect_failed_event.py b/src/kernel/types/browsers/browser_monitor_reconnect_failed_event.py new file mode 100644 index 00000000..79308d2e --- /dev/null +++ b/src/kernel/types/browsers/browser_monitor_reconnect_failed_event.py @@ -0,0 +1,37 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from .browser_event_source import BrowserEventSource + +__all__ = ["BrowserMonitorReconnectFailedEvent", "Data"] + + +class Data(BaseModel): + reason: Optional[Literal["reconnect_exhausted"]] = None + """Reason for the reconnection failure. + + reconnect_exhausted: all retry attempts were used up without successfully + restoring the CDP connection. + """ + + +class BrowserMonitorReconnectFailedEvent(BaseModel): + """ + The CDP connection to Chrome could not be re-established after exhausting all reconnection attempts. No further telemetry events will arrive on this session. + """ + + source: BrowserEventSource + """Provenance metadata identifying which producer emitted the event.""" + + ts: int + """Event timestamp in Unix microseconds.""" + + type: Literal["monitor_reconnect_failed"] + + data: Optional[Data] = None + + truncated: Optional[bool] = None + """True if the data field was truncated due to size limits.""" diff --git a/src/kernel/types/browsers/browser_monitor_reconnected_event.py b/src/kernel/types/browsers/browser_monitor_reconnected_event.py new file mode 100644 index 00000000..a49ad351 --- /dev/null +++ b/src/kernel/types/browsers/browser_monitor_reconnected_event.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from .browser_event_source import BrowserEventSource + +__all__ = ["BrowserMonitorReconnectedEvent", "Data"] + + +class Data(BaseModel): + reconnect_duration_ms: Optional[int] = None + """Wall-clock time in milliseconds taken to reconnect after the disconnection.""" + + +class BrowserMonitorReconnectedEvent(BaseModel): + """ + The CDP connection to Chrome was successfully re-established after a disconnection. Events emitted during the gap are lost. Computed state is reset, so navigation and network tracking restart fresh from this point. + """ + + source: BrowserEventSource + """Provenance metadata identifying which producer emitted the event.""" + + ts: int + """Event timestamp in Unix microseconds.""" + + type: Literal["monitor_reconnected"] + + data: Optional[Data] = None + + truncated: Optional[bool] = None + """True if the data field was truncated due to size limits.""" diff --git a/src/kernel/types/browsers/browser_monitor_screenshot_event.py b/src/kernel/types/browsers/browser_monitor_screenshot_event.py new file mode 100644 index 00000000..9b446fa9 --- /dev/null +++ b/src/kernel/types/browsers/browser_monitor_screenshot_event.py @@ -0,0 +1,31 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from .browser_event_source import BrowserEventSource + +__all__ = ["BrowserMonitorScreenshotEvent", "Data"] + + +class Data(BaseModel): + png: Optional[str] = None + """Base64-encoded PNG screenshot of the browser viewport.""" + + +class BrowserMonitorScreenshotEvent(BaseModel): + """A periodic screenshot of the browser viewport.""" + + source: BrowserEventSource + """Provenance metadata identifying which producer emitted the event.""" + + ts: int + """Event timestamp in Unix microseconds.""" + + type: Literal["monitor_screenshot"] + + data: Optional[Data] = None + + truncated: Optional[bool] = None + """True if the data field was truncated due to size limits.""" diff --git a/src/kernel/types/browsers/browser_network_idle_event.py b/src/kernel/types/browsers/browser_network_idle_event.py new file mode 100644 index 00000000..4b4fae6b --- /dev/null +++ b/src/kernel/types/browsers/browser_network_idle_event.py @@ -0,0 +1,34 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from .browser_event_source import BrowserEventSource +from .browser_event_context import BrowserEventContext + +__all__ = ["BrowserNetworkIdleEvent"] + + +class BrowserNetworkIdleEvent(BaseModel): + """ + A browser network idle event emitted after a 500ms quiet period with no in-flight HTTP requests. + """ + + source: BrowserEventSource + """Provenance metadata identifying which producer emitted the event.""" + + ts: int + """Event timestamp in Unix microseconds.""" + + type: Literal["network_idle"] + + data: Optional[BrowserEventContext] = None + """Browser event context stamped by the browser monitor onto all CDP-sourced + events. + + Identifies the target, frame, and navigation epoch in which the event occurred. + """ + + truncated: Optional[bool] = None + """True if the data field was truncated due to size limits.""" diff --git a/src/kernel/types/browsers/browser_network_loading_failed_event.py b/src/kernel/types/browsers/browser_network_loading_failed_event.py new file mode 100644 index 00000000..bf566a6b --- /dev/null +++ b/src/kernel/types/browsers/browser_network_loading_failed_event.py @@ -0,0 +1,58 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from .browser_event_source import BrowserEventSource +from .browser_event_context import BrowserEventContext + +__all__ = ["BrowserNetworkLoadingFailedEvent", "Data"] + + +class Data(BrowserEventContext): + """Browser event context stamped by the browser monitor onto all CDP-sourced events. + + Identifies the target, frame, and navigation epoch in which the event occurred. + """ + + canceled: Optional[bool] = None + """True if the request was canceled by the browser or page script.""" + + error_text: Optional[str] = None + """Network error description (e.g. net::ERR_CONNECTION_REFUSED).""" + + request_id: Optional[str] = None + """CDP request identifier matching the originating network_request event.""" + + resource_type: Optional[str] = None + """CDP Network.ResourceType for the request, passed through as-is from Chrome. + + Known values include Document, Fetch, XHR, Script, Stylesheet, Image, Media, + Font, TextTrack, EventSource, WebSocket, Manifest, Prefetch, Other, and more. + """ + + +class BrowserNetworkLoadingFailedEvent(BaseModel): + """A browser network loading failed event. + + If the request was already in flight when CDP attached (no prior network_request was emitted for it), url, frame_id, loader_id, and resource_type are absent; BrowserEventContext is partially populated in that case. + """ + + source: BrowserEventSource + """Provenance metadata identifying which producer emitted the event.""" + + ts: int + """Event timestamp in Unix microseconds.""" + + type: Literal["network_loading_failed"] + + data: Optional[Data] = None + """Browser event context stamped by the browser monitor onto all CDP-sourced + events. + + Identifies the target, frame, and navigation epoch in which the event occurred. + """ + + truncated: Optional[bool] = None + """True if the data field was truncated due to size limits.""" diff --git a/src/kernel/types/browsers/browser_network_request_event.py b/src/kernel/types/browsers/browser_network_request_event.py new file mode 100644 index 00000000..e1bd9428 --- /dev/null +++ b/src/kernel/types/browsers/browser_network_request_event.py @@ -0,0 +1,74 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from .browser_event_source import BrowserEventSource +from .browser_http_headers import BrowserHTTPHeaders +from .browser_event_context import BrowserEventContext + +__all__ = ["BrowserNetworkRequestEvent", "Data"] + + +class Data(BrowserEventContext): + """Browser event context stamped by the browser monitor onto all CDP-sourced events. + + Identifies the target, frame, and navigation epoch in which the event occurred. + """ + + document_url: Optional[str] = None + """URL of the document that initiated the request.""" + + headers: Optional[BrowserHTTPHeaders] = None + """Request headers.""" + + initiator_type: Optional[str] = None + """ + CDP Initiator.type indicating what caused the request, passed through as-is from + Chrome. Known values include script, parser, preload, and other. + """ + + is_redirect: Optional[bool] = None + """True if this request is the result of a redirect.""" + + method: Optional[str] = None + """HTTP method as sent on the wire (e.g. GET, POST).""" + + post_data: Optional[str] = None + """Request body for POST/PUT requests, if available.""" + + redirect_url: Optional[str] = None + """Original URL before the redirect, present when is_redirect is true.""" + + request_id: Optional[str] = None + """CDP request identifier, unique within the session.""" + + resource_type: Optional[str] = None + """CDP Network.ResourceType for the request, passed through as-is from Chrome. + + Known values include Document, Fetch, XHR, Script, Stylesheet, Image, Media, + Font, TextTrack, EventSource, WebSocket, Manifest, Prefetch, Other, and more. + """ + + +class BrowserNetworkRequestEvent(BaseModel): + """A browser network request sent event.""" + + source: BrowserEventSource + """Provenance metadata identifying which producer emitted the event.""" + + ts: int + """Event timestamp in Unix microseconds.""" + + type: Literal["network_request"] + + data: Optional[Data] = None + """Browser event context stamped by the browser monitor onto all CDP-sourced + events. + + Identifies the target, frame, and navigation epoch in which the event occurred. + """ + + truncated: Optional[bool] = None + """True if the data field was truncated due to size limits.""" diff --git a/src/kernel/types/browsers/browser_network_response_event.py b/src/kernel/types/browsers/browser_network_response_event.py new file mode 100644 index 00000000..8a71d24a --- /dev/null +++ b/src/kernel/types/browsers/browser_network_response_event.py @@ -0,0 +1,71 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from .browser_event_source import BrowserEventSource +from .browser_http_headers import BrowserHTTPHeaders +from .browser_event_context import BrowserEventContext + +__all__ = ["BrowserNetworkResponseEvent", "Data"] + + +class Data(BrowserEventContext): + """Browser event context stamped by the browser monitor onto all CDP-sourced events. + + Identifies the target, frame, and navigation epoch in which the event occurred. + """ + + body: Optional[str] = None + """Truncated response body, present only for text MIME types.""" + + headers: Optional[BrowserHTTPHeaders] = None + """Response headers.""" + + method: Optional[str] = None + """HTTP method of the original request.""" + + mime_type: Optional[str] = None + """MIME type of the response (e.g. text/html, application/json).""" + + request_id: Optional[str] = None + """CDP request identifier matching the originating network_request event.""" + + resource_type: Optional[str] = None + """CDP Network.ResourceType for the request, passed through as-is from Chrome. + + Known values include Document, Fetch, XHR, Script, Stylesheet, Image, Media, + Font, TextTrack, EventSource, WebSocket, Manifest, Prefetch, Other, and more. + """ + + status: Optional[int] = None + """HTTP response status code.""" + + status_text: Optional[str] = None + """HTTP response status text (e.g. OK, Not Found).""" + + +class BrowserNetworkResponseEvent(BaseModel): + """A browser network response received event. + + Fired after the response body is fully received, not when headers arrive. + """ + + source: BrowserEventSource + """Provenance metadata identifying which producer emitted the event.""" + + ts: int + """Event timestamp in Unix microseconds.""" + + type: Literal["network_response"] + + data: Optional[Data] = None + """Browser event context stamped by the browser monitor onto all CDP-sourced + events. + + Identifies the target, frame, and navigation epoch in which the event occurred. + """ + + truncated: Optional[bool] = None + """True if the data field was truncated due to size limits.""" diff --git a/src/kernel/types/browsers/browser_page_dom_content_loaded_event.py b/src/kernel/types/browsers/browser_page_dom_content_loaded_event.py new file mode 100644 index 00000000..e917ca32 --- /dev/null +++ b/src/kernel/types/browsers/browser_page_dom_content_loaded_event.py @@ -0,0 +1,45 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from .browser_event_source import BrowserEventSource +from .browser_event_context import BrowserEventContext + +__all__ = ["BrowserPageDomContentLoadedEvent", "Data"] + + +class Data(BrowserEventContext): + """Browser event context stamped by the browser monitor onto all CDP-sourced events. + + Identifies the target, frame, and navigation epoch in which the event occurred. + """ + + cdp_timestamp: Optional[float] = None + """ + Chrome monotonic clock value in seconds at which DOMContentLoaded fired, + relative to browser process start (not Unix epoch). Use ts for wall-clock time. + """ + + +class BrowserPageDomContentLoadedEvent(BaseModel): + """A browser DOMContentLoaded event (CDP Page.domContentEventFired).""" + + source: BrowserEventSource + """Provenance metadata identifying which producer emitted the event.""" + + ts: int + """Event timestamp in Unix microseconds.""" + + type: Literal["page_dom_content_loaded"] + + data: Optional[Data] = None + """Browser event context stamped by the browser monitor onto all CDP-sourced + events. + + Identifies the target, frame, and navigation epoch in which the event occurred. + """ + + truncated: Optional[bool] = None + """True if the data field was truncated due to size limits.""" diff --git a/src/kernel/types/browsers/browser_page_layout_settled_event.py b/src/kernel/types/browsers/browser_page_layout_settled_event.py new file mode 100644 index 00000000..289b53a5 --- /dev/null +++ b/src/kernel/types/browsers/browser_page_layout_settled_event.py @@ -0,0 +1,34 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from .browser_event_source import BrowserEventSource +from .browser_event_context import BrowserEventContext + +__all__ = ["BrowserPageLayoutSettledEvent"] + + +class BrowserPageLayoutSettledEvent(BaseModel): + """ + A browser layout settled event emitted 1 second after page load with no intervening layout shifts, indicating visual stability. Each layout shift resets the 1-second timer. + """ + + source: BrowserEventSource + """Provenance metadata identifying which producer emitted the event.""" + + ts: int + """Event timestamp in Unix microseconds.""" + + type: Literal["page_layout_settled"] + + data: Optional[BrowserEventContext] = None + """Browser event context stamped by the browser monitor onto all CDP-sourced + events. + + Identifies the target, frame, and navigation epoch in which the event occurred. + """ + + truncated: Optional[bool] = None + """True if the data field was truncated due to size limits.""" diff --git a/src/kernel/types/browsers/browser_page_layout_shift_event.py b/src/kernel/types/browsers/browser_page_layout_shift_event.py new file mode 100644 index 00000000..336bfce2 --- /dev/null +++ b/src/kernel/types/browsers/browser_page_layout_shift_event.py @@ -0,0 +1,69 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from .browser_event_source import BrowserEventSource +from .browser_event_context import BrowserEventContext + +__all__ = ["BrowserPageLayoutShiftEvent", "Data", "DataLayoutShiftDetails"] + + +class DataLayoutShiftDetails(BaseModel): + """PerformanceLayoutShift attributes from the Performance Timeline entry.""" + + had_recent_input: Optional[bool] = None + """ + True if the layout shift was preceded by user input within 500ms, excluding it + from CLS. + """ + + value: Optional[float] = None + """Layout shift score for this entry (contribution to CLS).""" + + +class Data(BrowserEventContext): + """Browser event context stamped by the browser monitor onto all CDP-sourced events. + + Identifies the target, frame, and navigation epoch in which the event occurred. + """ + + duration: Optional[float] = None + """ + Duration of the layout shift entry in milliseconds (always 0 for layout shifts + per spec). + """ + + layout_shift_details: Optional[DataLayoutShiftDetails] = None + """PerformanceLayoutShift attributes from the Performance Timeline entry.""" + + source_frame_id: Optional[str] = None + """CDP frame identifier of the frame where the layout shift occurred.""" + + time: Optional[float] = None + """Performance Timeline timestamp of the layout shift in milliseconds.""" + + +class BrowserPageLayoutShiftEvent(BaseModel): + """ + A browser cumulative layout shift (CLS) event from the Performance Timeline API. + """ + + source: BrowserEventSource + """Provenance metadata identifying which producer emitted the event.""" + + ts: int + """Event timestamp in Unix microseconds.""" + + type: Literal["page_layout_shift"] + + data: Optional[Data] = None + """Browser event context stamped by the browser monitor onto all CDP-sourced + events. + + Identifies the target, frame, and navigation epoch in which the event occurred. + """ + + truncated: Optional[bool] = None + """True if the data field was truncated due to size limits.""" diff --git a/src/kernel/types/browsers/browser_page_lcp_event.py b/src/kernel/types/browsers/browser_page_lcp_event.py new file mode 100644 index 00000000..13b9c270 --- /dev/null +++ b/src/kernel/types/browsers/browser_page_lcp_event.py @@ -0,0 +1,75 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from .browser_event_source import BrowserEventSource +from .browser_event_context import BrowserEventContext + +__all__ = ["BrowserPageLcpEvent", "Data", "DataLcpDetails"] + + +class DataLcpDetails(BaseModel): + """LargestContentfulPaint attributes from the Performance Timeline entry.""" + + element_id: Optional[str] = None + """id attribute of the LCP element, if present.""" + + load_time: Optional[float] = None + """Load time of the LCP element in milliseconds.""" + + node_id: Optional[int] = None + """CDP DOM node identifier of the LCP element.""" + + render_time: Optional[float] = None + """ + Render time of the LCP element in milliseconds; 0 for cross-origin images + without Timing-Allow-Origin. + """ + + size: Optional[float] = None + """Visible area of the LCP element in pixels squared.""" + + url: Optional[str] = None + """URL of the LCP element for image or video elements.""" + + +class Data(BrowserEventContext): + """Browser event context stamped by the browser monitor onto all CDP-sourced events. + + Identifies the target, frame, and navigation epoch in which the event occurred. + """ + + lcp_details: Optional[DataLcpDetails] = None + """LargestContentfulPaint attributes from the Performance Timeline entry.""" + + source_frame_id: Optional[str] = None + """CDP frame identifier of the frame where the LCP element was rendered.""" + + time: Optional[float] = None + """Performance Timeline timestamp of the LCP entry in milliseconds.""" + + +class BrowserPageLcpEvent(BaseModel): + """ + A browser Largest Contentful Paint (LCP) event from the Performance Timeline API. + """ + + source: BrowserEventSource + """Provenance metadata identifying which producer emitted the event.""" + + ts: int + """Event timestamp in Unix microseconds.""" + + type: Literal["page_lcp"] + + data: Optional[Data] = None + """Browser event context stamped by the browser monitor onto all CDP-sourced + events. + + Identifies the target, frame, and navigation epoch in which the event occurred. + """ + + truncated: Optional[bool] = None + """True if the data field was truncated due to size limits.""" diff --git a/src/kernel/types/browsers/browser_page_load_event.py b/src/kernel/types/browsers/browser_page_load_event.py new file mode 100644 index 00000000..b4d85c8f --- /dev/null +++ b/src/kernel/types/browsers/browser_page_load_event.py @@ -0,0 +1,45 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from .browser_event_source import BrowserEventSource +from .browser_event_context import BrowserEventContext + +__all__ = ["BrowserPageLoadEvent", "Data"] + + +class Data(BrowserEventContext): + """Browser event context stamped by the browser monitor onto all CDP-sourced events. + + Identifies the target, frame, and navigation epoch in which the event occurred. + """ + + cdp_timestamp: Optional[float] = None + """ + Chrome monotonic clock value in seconds at which the load event fired, relative + to browser process start (not Unix epoch). Use ts for wall-clock time. + """ + + +class BrowserPageLoadEvent(BaseModel): + """A browser page load event (CDP Page.loadEventFired).""" + + source: BrowserEventSource + """Provenance metadata identifying which producer emitted the event.""" + + ts: int + """Event timestamp in Unix microseconds.""" + + type: Literal["page_load"] + + data: Optional[Data] = None + """Browser event context stamped by the browser monitor onto all CDP-sourced + events. + + Identifies the target, frame, and navigation epoch in which the event occurred. + """ + + truncated: Optional[bool] = None + """True if the data field was truncated due to size limits.""" diff --git a/src/kernel/types/browsers/browser_page_navigation_event.py b/src/kernel/types/browsers/browser_page_navigation_event.py new file mode 100644 index 00000000..f857398c --- /dev/null +++ b/src/kernel/types/browsers/browser_page_navigation_event.py @@ -0,0 +1,55 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from .browser_event_source import BrowserEventSource + +__all__ = ["BrowserPageNavigationEvent", "Data"] + + +class Data(BaseModel): + frame_id: Optional[str] = None + """CDP frame identifier of the navigated frame.""" + + loader_id: Optional[str] = None + """New CDP document loader identifier assigned for this navigation.""" + + parent_frame_id: Optional[str] = None + """ + Parent frame identifier for subframe navigations; absent for top-level + navigations. + """ + + session_id: Optional[str] = None + """CDP session identifier.""" + + target_id: Optional[str] = None + """Browser target identifier.""" + + target_type: Optional[Literal["page", "background_page", "service_worker", "shared_worker", "other"]] = None + """CDP target type of the page that produced the event.""" + + url: Optional[str] = None + """URL navigated to.""" + + +class BrowserPageNavigationEvent(BaseModel): + """A browser page navigation started event (CDP Page.frameNavigated). + + Carries nav context fields inline but not nav_seq, as this event resets the navigation epoch. + """ + + source: BrowserEventSource + """Provenance metadata identifying which producer emitted the event.""" + + ts: int + """Event timestamp in Unix microseconds.""" + + type: Literal["page_navigation"] + + data: Optional[Data] = None + + truncated: Optional[bool] = None + """True if the data field was truncated due to size limits.""" diff --git a/src/kernel/types/browsers/browser_page_navigation_settled_event.py b/src/kernel/types/browsers/browser_page_navigation_settled_event.py new file mode 100644 index 00000000..b5227ff4 --- /dev/null +++ b/src/kernel/types/browsers/browser_page_navigation_settled_event.py @@ -0,0 +1,34 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from .browser_event_source import BrowserEventSource +from .browser_event_context import BrowserEventContext + +__all__ = ["BrowserPageNavigationSettledEvent"] + + +class BrowserPageNavigationSettledEvent(BaseModel): + """ + Emitted when page_dom_content_loaded and page_layout_settled have both fired for the same navigation, indicating the page is loaded and visually stable. Independent of network_idle; a single pending request does not block it. + """ + + source: BrowserEventSource + """Provenance metadata identifying which producer emitted the event.""" + + ts: int + """Event timestamp in Unix microseconds.""" + + type: Literal["page_navigation_settled"] + + data: Optional[BrowserEventContext] = None + """Browser event context stamped by the browser monitor onto all CDP-sourced + events. + + Identifies the target, frame, and navigation epoch in which the event occurred. + """ + + truncated: Optional[bool] = None + """True if the data field was truncated due to size limits.""" diff --git a/src/kernel/types/browsers/browser_page_tab_opened_event.py b/src/kernel/types/browsers/browser_page_tab_opened_event.py new file mode 100644 index 00000000..9831aee1 --- /dev/null +++ b/src/kernel/types/browsers/browser_page_tab_opened_event.py @@ -0,0 +1,45 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from .browser_event_source import BrowserEventSource + +__all__ = ["BrowserPageTabOpenedEvent", "Data"] + + +class Data(BaseModel): + opener_id: Optional[str] = None + """Target identifier of the tab that opened this one, if any.""" + + target_id: Optional[str] = None + """CDP target identifier for the newly opened tab.""" + + target_type: Optional[Literal["page", "background_page", "service_worker", "shared_worker", "other"]] = None + """CDP target type of the page that produced the event.""" + + title: Optional[str] = None + """Initial page title of the new tab.""" + + url: Optional[str] = None + """Initial URL of the new tab.""" + + +class BrowserPageTabOpenedEvent(BaseModel): + """ + A new browser tab or target was opened (CDP Target.attachedToTarget for page targets). Fires before a CDP session is attached to the new target, so session_id, frame_id, loader_id, and nav_seq are absent; this event does not compose BrowserEventContext. Consumers reading context fields generically should treat it as a special case. + """ + + source: BrowserEventSource + """Provenance metadata identifying which producer emitted the event.""" + + ts: int + """Event timestamp in Unix microseconds.""" + + type: Literal["page_tab_opened"] + + data: Optional[Data] = None + + truncated: Optional[bool] = None + """True if the data field was truncated due to size limits.""" diff --git a/src/kernel/types/browsers/browser_telemetry_categories_config.py b/src/kernel/types/browsers/browser_telemetry_categories_config.py new file mode 100644 index 00000000..82cdb1db --- /dev/null +++ b/src/kernel/types/browsers/browser_telemetry_categories_config.py @@ -0,0 +1,32 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel +from .browser_telemetry_category_config import BrowserTelemetryCategoryConfig + +__all__ = ["BrowserTelemetryCategoriesConfig"] + + +class BrowserTelemetryCategoriesConfig(BaseModel): + """Per-category telemetry capture settings.""" + + console: Optional[BrowserTelemetryCategoryConfig] = None + """Console output (log, warn, error) and uncaught exceptions.""" + + interaction: Optional[BrowserTelemetryCategoryConfig] = None + """User interaction events including clicks, keydowns, and scroll-settled events.""" + + network: Optional[BrowserTelemetryCategoryConfig] = None + """ + HTTP request and response metadata including URL, method, status code, and + timing. Request post data is forwarded as-is from CDP. Text response bodies are + truncated at 8 KB for structured types (JSON, XML, form data) and 4 KB for other + text types. Binary responses (images, fonts, media) are excluded. + """ + + page: Optional[BrowserTelemetryCategoryConfig] = None + """ + Page lifecycle events including navigation, DOMContentLoaded, load, layout + shifts, and LCP. + """ diff --git a/src/kernel/types/browsers/browser_telemetry_categories_config_param.py b/src/kernel/types/browsers/browser_telemetry_categories_config_param.py new file mode 100644 index 00000000..75a3a797 --- /dev/null +++ b/src/kernel/types/browsers/browser_telemetry_categories_config_param.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +from .browser_telemetry_category_config_param import BrowserTelemetryCategoryConfigParam + +__all__ = ["BrowserTelemetryCategoriesConfigParam"] + + +class BrowserTelemetryCategoriesConfigParam(TypedDict, total=False): + """Per-category telemetry capture settings.""" + + console: BrowserTelemetryCategoryConfigParam + """Console output (log, warn, error) and uncaught exceptions.""" + + interaction: BrowserTelemetryCategoryConfigParam + """User interaction events including clicks, keydowns, and scroll-settled events.""" + + network: BrowserTelemetryCategoryConfigParam + """ + HTTP request and response metadata including URL, method, status code, and + timing. Request post data is forwarded as-is from CDP. Text response bodies are + truncated at 8 KB for structured types (JSON, XML, form data) and 4 KB for other + text types. Binary responses (images, fonts, media) are excluded. + """ + + page: BrowserTelemetryCategoryConfigParam + """ + Page lifecycle events including navigation, DOMContentLoaded, load, layout + shifts, and LCP. + """ diff --git a/src/kernel/types/browsers/browser_telemetry_category_config.py b/src/kernel/types/browsers/browser_telemetry_category_config.py new file mode 100644 index 00000000..4a2da2cb --- /dev/null +++ b/src/kernel/types/browsers/browser_telemetry_category_config.py @@ -0,0 +1,14 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = ["BrowserTelemetryCategoryConfig"] + + +class BrowserTelemetryCategoryConfig(BaseModel): + """Per-category telemetry configuration.""" + + enabled: Optional[bool] = None + """Whether this category is captured. Defaults to true if omitted.""" diff --git a/src/kernel/types/browsers/browser_telemetry_category_config_param.py b/src/kernel/types/browsers/browser_telemetry_category_config_param.py new file mode 100644 index 00000000..3824b4c8 --- /dev/null +++ b/src/kernel/types/browsers/browser_telemetry_category_config_param.py @@ -0,0 +1,14 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["BrowserTelemetryCategoryConfigParam"] + + +class BrowserTelemetryCategoryConfigParam(TypedDict, total=False): + """Per-category telemetry configuration.""" + + enabled: bool + """Whether this category is captured. Defaults to true if omitted.""" diff --git a/src/kernel/types/browsers/browser_telemetry_config.py b/src/kernel/types/browsers/browser_telemetry_config.py new file mode 100644 index 00000000..d4365eeb --- /dev/null +++ b/src/kernel/types/browsers/browser_telemetry_config.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel +from .browser_telemetry_categories_config import BrowserTelemetryCategoriesConfig + +__all__ = ["BrowserTelemetryConfig"] + + +class BrowserTelemetryConfig(BaseModel): + """Telemetry configuration for a browser session.""" + + browser: Optional[BrowserTelemetryCategoriesConfig] = None + """Per-category enable/disable flags. If omitted, all categories are captured.""" diff --git a/src/kernel/types/browsers/browser_telemetry_config_param.py b/src/kernel/types/browsers/browser_telemetry_config_param.py new file mode 100644 index 00000000..33464340 --- /dev/null +++ b/src/kernel/types/browsers/browser_telemetry_config_param.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +from .browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam + +__all__ = ["BrowserTelemetryConfigParam"] + + +class BrowserTelemetryConfigParam(TypedDict, total=False): + """Telemetry configuration for a browser session.""" + + browser: BrowserTelemetryCategoriesConfigParam + """Per-category enable/disable flags. If omitted, all categories are captured.""" diff --git a/src/kernel/types/browsers/browser_telemetry_event.py b/src/kernel/types/browsers/browser_telemetry_event.py new file mode 100644 index 00000000..c27297bc --- /dev/null +++ b/src/kernel/types/browsers/browser_telemetry_event.py @@ -0,0 +1,61 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union +from typing_extensions import Annotated, TypeAlias + +from ..._utils import PropertyInfo +from .browser_page_lcp_event import BrowserPageLcpEvent +from .browser_page_load_event import BrowserPageLoadEvent +from .browser_network_idle_event import BrowserNetworkIdleEvent +from .browser_interaction_key_event import BrowserInteractionKeyEvent +from .browser_network_request_event import BrowserNetworkRequestEvent +from .browser_page_navigation_event import BrowserPageNavigationEvent +from .browser_page_tab_opened_event import BrowserPageTabOpenedEvent +from .browser_network_response_event import BrowserNetworkResponseEvent +from .browser_interaction_click_event import BrowserInteractionClickEvent +from .browser_page_layout_shift_event import BrowserPageLayoutShiftEvent +from .browser_monitor_screenshot_event import BrowserMonitorScreenshotEvent +from .browser_monitor_init_failed_event import BrowserMonitorInitFailedEvent +from .browser_monitor_reconnected_event import BrowserMonitorReconnectedEvent +from .browser_page_layout_settled_event import BrowserPageLayoutSettledEvent +from .browser_monitor_disconnected_event import BrowserMonitorDisconnectedEvent +from .browser_network_loading_failed_event import BrowserNetworkLoadingFailedEvent +from .browser_page_dom_content_loaded_event import BrowserPageDomContentLoadedEvent +from .browser_page_navigation_settled_event import BrowserPageNavigationSettledEvent +from .browser_monitor_reconnect_failed_event import BrowserMonitorReconnectFailedEvent +from .browser_interaction_scroll_settled_event import BrowserInteractionScrollSettledEvent + +__all__ = ["BrowserTelemetryEvent"] + +BrowserTelemetryEvent: TypeAlias = Annotated[ + Union[ + "BrowserConsoleLogEvent", + "BrowserConsoleErrorEvent", + BrowserNetworkRequestEvent, + BrowserNetworkResponseEvent, + BrowserNetworkLoadingFailedEvent, + BrowserNetworkIdleEvent, + BrowserPageNavigationEvent, + BrowserPageDomContentLoadedEvent, + BrowserPageLoadEvent, + BrowserPageTabOpenedEvent, + BrowserPageLayoutShiftEvent, + BrowserPageLcpEvent, + BrowserPageLayoutSettledEvent, + BrowserPageNavigationSettledEvent, + BrowserInteractionClickEvent, + BrowserInteractionKeyEvent, + BrowserInteractionScrollSettledEvent, + BrowserMonitorScreenshotEvent, + BrowserMonitorDisconnectedEvent, + BrowserMonitorReconnectedEvent, + BrowserMonitorReconnectFailedEvent, + BrowserMonitorInitFailedEvent, + ], + PropertyInfo(discriminator="type"), +] + +from .browser_console_log_event import BrowserConsoleLogEvent +from .browser_console_error_event import BrowserConsoleErrorEvent diff --git a/src/kernel/types/browsers/telemetry_stream_response.py b/src/kernel/types/browsers/telemetry_stream_response.py new file mode 100644 index 00000000..fef04b3f --- /dev/null +++ b/src/kernel/types/browsers/telemetry_stream_response.py @@ -0,0 +1,34 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from ..._models import BaseModel + +__all__ = ["TelemetryStreamResponse"] + + +class TelemetryStreamResponse(BaseModel): + """Envelope wrapping a browser telemetry event with its monotonic sequence number. + + Each SSE data: frame carries one envelope as JSON. The seq value is also emitted as the SSE id: field so clients can pass it as Last-Event-ID on reconnect. + """ + + event: "BrowserTelemetryEvent" + """Union type representing any browser telemetry event. + + Discriminated on `type`. Events with a `monitor_` prefix (monitor_screenshot, + monitor_disconnected, monitor_reconnected, monitor_reconnect_failed, + monitor_init_failed) are always emitted regardless of the category configuration + in BrowserTelemetryConfig. All other event types are controlled by the + per-category enable/disable flags. + """ + + seq: int + """Process-monotonic sequence number assigned by the browser VM. + + Pass as Last-Event-ID on reconnect to resume without gaps. Gaps in received seq + values indicate dropped events. + """ + + +from .browser_telemetry_event import BrowserTelemetryEvent diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py index 7ff4910a..7c60ae22 100644 --- a/src/kernel/types/invocation_list_browsers_response.py +++ b/src/kernel/types/invocation_list_browsers_response.py @@ -9,6 +9,7 @@ from .browser_pool_ref import BrowserPoolRef from .browser_persistence import BrowserPersistence from .shared.browser_viewport import BrowserViewport +from .browsers.browser_telemetry_config import BrowserTelemetryConfig __all__ = ["InvocationListBrowsersResponse", "Browser"] @@ -76,7 +77,16 @@ class Browser(BaseModel): """ID of the proxy associated with this browser session, if any.""" start_url: Optional[str] = None - """Start URL requested for the session, if provided.""" + """URL the session was asked to navigate to on creation, if any. + + Recorded for debugging. Navigation is fire-and-forget — the URL is dispatched to + the browser without waiting for it to load, and any errors (DNS failure, bad + status, timeout) are silently dropped. Captures what was requested, not what the + browser actually loaded. + """ + + telemetry: Optional[BrowserTelemetryConfig] = None + """Active telemetry configuration for the session, if any.""" usage: Optional[BrowserUsage] = None """Session usage metrics.""" diff --git a/src/kernel/types/proxy_check_params.py b/src/kernel/types/proxy_check_params.py index de3a8c3d..47f99f04 100644 --- a/src/kernel/types/proxy_check_params.py +++ b/src/kernel/types/proxy_check_params.py @@ -19,5 +19,8 @@ class ProxyCheckParams(TypedDict, total=False): proxies, the exit node changes between requests, so a successful check validates proxy configuration but does not guarantee that a subsequent browser session will use the same exit IP or reach the same site — it is useful for verifying - credentials and connectivity, not for predicting site-specific behavior. + credentials and connectivity, not for predicting site-specific behavior. When + provided, the check result does not update the proxy's health status, since a + failure may indicate a problem with the target site rather than the proxy + itself. """ diff --git a/tests/api_resources/browsers/test_telemetry.py b/tests/api_resources/browsers/test_telemetry.py new file mode 100644 index 00000000..0fcd2715 --- /dev/null +++ b/tests/api_resources/browsers/test_telemetry.py @@ -0,0 +1,122 @@ +# 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 kernel import Kernel, AsyncKernel + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestTelemetry: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_stream(self, client: Kernel) -> None: + telemetry_stream = client.browsers.telemetry.stream( + id="id", + ) + telemetry_stream.response.close() + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_stream_with_all_params(self, client: Kernel) -> None: + telemetry_stream = client.browsers.telemetry.stream( + id="id", + last_event_id="Last-Event-ID", + ) + telemetry_stream.response.close() + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_stream(self, client: Kernel) -> None: + response = client.browsers.telemetry.with_raw_response.stream( + id="id", + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = response.parse() + stream.close() + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_stream(self, client: Kernel) -> None: + with client.browsers.telemetry.with_streaming_response.stream( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = response.parse() + stream.close() + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_stream(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.telemetry.with_raw_response.stream( + id="", + ) + + +class TestAsyncTelemetry: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_stream(self, async_client: AsyncKernel) -> None: + telemetry_stream = await async_client.browsers.telemetry.stream( + id="id", + ) + await telemetry_stream.response.aclose() + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_stream_with_all_params(self, async_client: AsyncKernel) -> None: + telemetry_stream = await async_client.browsers.telemetry.stream( + id="id", + last_event_id="Last-Event-ID", + ) + await telemetry_stream.response.aclose() + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_stream(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.telemetry.with_raw_response.stream( + id="id", + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = await response.parse() + await stream.close() + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_stream(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.telemetry.with_streaming_response.stream( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = await response.parse() + await stream.close() + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_stream(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.telemetry.with_raw_response.stream( + id="", + ) diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index 3d1ac191..dcd20977 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -56,6 +56,14 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: proxy_id="proxy_id", start_url="https://example.com", stealth=True, + telemetry={ + "browser": { + "console": {"enabled": True}, + "interaction": {"enabled": True}, + "network": {"enabled": True}, + "page": {"enabled": True}, + } + }, timeout_seconds=10, viewport={ "height": 800, @@ -158,6 +166,14 @@ def test_method_update_with_all_params(self, client: Kernel) -> None: "save_changes": True, }, proxy_id="proxy_id", + telemetry={ + "browser": { + "console": {"enabled": True}, + "interaction": {"enabled": True}, + "network": {"enabled": True}, + "page": {"enabled": True}, + } + }, viewport={ "height": 800, "width": 1280, @@ -483,6 +499,14 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> proxy_id="proxy_id", start_url="https://example.com", stealth=True, + telemetry={ + "browser": { + "console": {"enabled": True}, + "interaction": {"enabled": True}, + "network": {"enabled": True}, + "page": {"enabled": True}, + } + }, timeout_seconds=10, viewport={ "height": 800, @@ -585,6 +609,14 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> "save_changes": True, }, proxy_id="proxy_id", + telemetry={ + "browser": { + "console": {"enabled": True}, + "interaction": {"enabled": True}, + "network": {"enabled": True}, + "page": {"enabled": True}, + } + }, viewport={ "height": 800, "width": 1280, From 6471847aa69c6edad3552199eea27b0ae10f931b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 16:38:06 +0000 Subject: [PATCH 401/448] feat(api): type can_reauth_reason as an enum on ManagedAuth --- .stats.yml | 4 +- src/kernel/types/auth/managed_auth.py | 59 ++++++++++++++++++++++++--- 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/.stats.yml b/.stats.yml index 3dfe6c63..01ff0714 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 113 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-a9c7e806132001dbfbd4e8ae6c7d0935e503f457d63385fc800c862e3d064375.yml -openapi_spec_hash: b048dcb0c5401bc0a301c3d30cb8ecba +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-2118a79fa452fad5922776130ea4877059e9568b603ef146171b7bb6a0f8458a.yml +openapi_spec_hash: 686e8c5855ed88b2944b9d9ec21d22ce config_hash: 37661d89120558d34b6cc184292632f2 diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py index 893631d4..3addc5e0 100644 --- a/src/kernel/types/auth/managed_auth.py +++ b/src/kernel/types/auth/managed_auth.py @@ -187,12 +187,61 @@ class ManagedAuth(BaseModel): can_reauth: Optional[bool] = None """ - Whether automatic re-authentication is possible (has credential, selectors, and - login_url) + Whether Kernel can automatically re-authenticate this connection when the + session expires. Requires a prior successful login plus either a Kernel + credential or an external credential reference. See `can_reauth_reason` for the + specific outcome. + """ + + can_reauth_reason: Optional[ + Literal[ + "external_credential", + "cua_has_credential", + "has_credential", + "viable_plans_found", + "no_requirements_recorded", + "requirements_satisfiable", + "no_prior_successful_login", + "no_credential", + "no_viable_plans", + "viable_plans_require_external_action", + "requires_external_action", + "requires_totp_without_secret", + "requires_sms_code", + "requires_email_code", + ] + ] = None + """ + Machine-readable reason for the current value of `can_reauth`. Affirmative + values (re-auth is possible): + + - `external_credential` — an external credential provider is attached + - `cua_has_credential` — CUA flow with a stored credential + - `has_credential` — Kernel credential is attached (optimistic; plan viability + not checked) + - `viable_plans_found` — at least one stored login plan can be replayed + - `no_requirements_recorded` — no recorded credential requirements to fail + against + - `requirements_satisfiable` — recorded requirements can be met by the attached + credential + + Negative values (a human must complete the login flow): + + - `no_prior_successful_login` — connection has never completed a successful + login + - `no_credential` — no Kernel or external credential attached + - `no_viable_plans` — credential attached but no replayable login plan exists + yet + - `viable_plans_require_external_action` — stored plans need an external step + (email link, push, etc.) + - `requires_external_action` — recorded requirements include an external step + - `requires_totp_without_secret` — flow needs a TOTP code but no TOTP secret is + stored + - `requires_sms_code` — flow needs an SMS code that cannot be received + automatically + - `requires_email_code` — flow needs an email code that cannot be received + automatically """ - - can_reauth_reason: Optional[str] = None - """Reason why automatic re-authentication is or is not possible""" credential: Optional[Credential] = None """Reference to credentials for the auth connection. Use one of: From a0cd19ca351ea892f1ec55be55ab5dc3d71bf1b8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 16:48:38 +0000 Subject: [PATCH 402/448] feat: EOL persistent browsers: openapi limits --- .stats.yml | 8 +- README.md | 4 +- api.md | 2 - src/kernel/resources/browsers/browsers.py | 121 +----------------- src/kernel/resources/projects/limits.py | 10 -- src/kernel/types/__init__.py | 3 - src/kernel/types/browser_create_params.py | 7 +- src/kernel/types/browser_create_response.py | 4 - src/kernel/types/browser_delete_params.py | 12 -- src/kernel/types/browser_list_response.py | 4 - src/kernel/types/browser_persistence.py | 12 -- src/kernel/types/browser_persistence_param.py | 14 -- .../types/browser_pool_acquire_response.py | 4 - src/kernel/types/browser_retrieve_response.py | 4 - src/kernel/types/browser_update_response.py | 4 - .../invocation_list_browsers_response.py | 4 - .../types/projects/limit_update_params.py | 6 - src/kernel/types/projects/project_limits.py | 6 - tests/api_resources/projects/test_limits.py | 2 - tests/api_resources/test_browsers.py | 80 ------------ 20 files changed, 9 insertions(+), 302 deletions(-) delete mode 100644 src/kernel/types/browser_delete_params.py delete mode 100644 src/kernel/types/browser_persistence.py delete mode 100644 src/kernel/types/browser_persistence_param.py diff --git a/.stats.yml b/.stats.yml index 01ff0714..6007fff2 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 113 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-2118a79fa452fad5922776130ea4877059e9568b603ef146171b7bb6a0f8458a.yml -openapi_spec_hash: 686e8c5855ed88b2944b9d9ec21d22ce -config_hash: 37661d89120558d34b6cc184292632f2 +configured_endpoints: 112 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-346b1affebf16e7f943bcf4db307a5589fcae636c9147dd0afbe5c02cf12ad30.yml +openapi_spec_hash: 348df436759d220264f12450919211c3 +config_hash: ae3dea7997fb5d36fa41979f9585ed78 diff --git a/README.md b/README.md index 2ac7afba..d7639da5 100644 --- a/README.md +++ b/README.md @@ -203,9 +203,9 @@ from kernel import Kernel client = Kernel() browser = client.browsers.create( - persistence={"id": "my-awesome-browser-for-user-1234"}, + profile={}, ) -print(browser.persistence) +print(browser.profile) ``` ## File uploads diff --git a/api.md b/api.md index d0b1d720..b652f86d 100644 --- a/api.md +++ b/api.md @@ -80,7 +80,6 @@ Types: ```python from kernel.types import ( - BrowserPersistence, BrowserPoolRef, BrowserUsage, Profile, @@ -98,7 +97,6 @@ Methods: - client.browsers.retrieve(id, \*\*params) -> BrowserRetrieveResponse - client.browsers.update(id, \*\*params) -> BrowserUpdateResponse - client.browsers.list(\*\*params) -> SyncOffsetPagination[BrowserListResponse] -- client.browsers.delete(\*\*params) -> None - client.browsers.curl(id, \*\*params) -> BrowserCurlResponse - client.browsers.delete_by_id(id) -> None - client.browsers.load_extensions(id, \*\*params) -> None diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 54010ea6..5d10f7f9 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -2,7 +2,6 @@ from __future__ import annotations -import typing_extensions from typing import Dict, Mapping, Iterable, Optional, cast from typing_extensions import Literal @@ -28,7 +27,6 @@ browser_curl_params, browser_list_params, browser_create_params, - browser_delete_params, browser_update_params, browser_retrieve_params, browser_load_extensions_params, @@ -90,7 +88,6 @@ from ...types.browser_list_response import BrowserListResponse from ...types.browser_create_response import BrowserCreateResponse from ...types.browser_update_response import BrowserUpdateResponse -from ...types.browser_persistence_param import BrowserPersistenceParam from ...types.browser_retrieve_response import BrowserRetrieveResponse from ...types.shared_params.browser_profile import BrowserProfile from ...types.shared_params.browser_viewport import BrowserViewport @@ -165,7 +162,6 @@ def create( headless: bool | Omit = omit, invocation_id: str | Omit = omit, kiosk_mode: bool | Omit = omit, - persistence: BrowserPersistenceParam | Omit = omit, profile: BrowserProfile | Omit = omit, proxy_id: str | Omit = omit, start_url: str | Omit = omit, @@ -186,8 +182,7 @@ def create( Args: chrome_policy: Custom Chrome enterprise policy overrides applied to this browser session. Keys are Chrome enterprise policy names; values must match their expected types. - Blocked: kernel-managed policies (extensions, proxy, CDP/automation). Ignored - when reusing an existing persistent session. See + Blocked: kernel-managed policies (extensions, proxy, CDP/automation). See https://chromeenterprise.google/policies/ extensions: List of browser extensions to load into the session. Provide each by id or name. @@ -203,8 +198,6 @@ def create( kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live view. - persistence: DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead. - profile: Profile selection for the browser session. Provide either id or name. If specified, the matching profile will be loaded into the browser session. Profiles must be created beforehand. @@ -260,7 +253,6 @@ def create( "headless": headless, "invocation_id": invocation_id, "kiosk_mode": kiosk_mode, - "persistence": persistence, "profile": profile, "proxy_id": proxy_id, "start_url": start_url, @@ -446,47 +438,6 @@ def list( model=BrowserListResponse, ) - @typing_extensions.deprecated("deprecated") - def delete( - self, - *, - persistent_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: - """DEPRECATED: Use DELETE /browsers/{id} instead. - - Delete a persistent browser - session by its persistent_id. - - Args: - persistent_id: Persistent browser identifier - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return self._delete( - "/browsers", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=maybe_transform({"persistent_id": persistent_id}, browser_delete_params.BrowserDeleteParams), - ), - cast_to=NoneType, - ) - def curl( self, id: str, @@ -697,7 +648,6 @@ async def create( headless: bool | Omit = omit, invocation_id: str | Omit = omit, kiosk_mode: bool | Omit = omit, - persistence: BrowserPersistenceParam | Omit = omit, profile: BrowserProfile | Omit = omit, proxy_id: str | Omit = omit, start_url: str | Omit = omit, @@ -718,8 +668,7 @@ async def create( Args: chrome_policy: Custom Chrome enterprise policy overrides applied to this browser session. Keys are Chrome enterprise policy names; values must match their expected types. - Blocked: kernel-managed policies (extensions, proxy, CDP/automation). Ignored - when reusing an existing persistent session. See + Blocked: kernel-managed policies (extensions, proxy, CDP/automation). See https://chromeenterprise.google/policies/ extensions: List of browser extensions to load into the session. Provide each by id or name. @@ -735,8 +684,6 @@ async def create( kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live view. - persistence: DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead. - profile: Profile selection for the browser session. Provide either id or name. If specified, the matching profile will be loaded into the browser session. Profiles must be created beforehand. @@ -792,7 +739,6 @@ async def create( "headless": headless, "invocation_id": invocation_id, "kiosk_mode": kiosk_mode, - "persistence": persistence, "profile": profile, "proxy_id": proxy_id, "start_url": start_url, @@ -978,49 +924,6 @@ def list( model=BrowserListResponse, ) - @typing_extensions.deprecated("deprecated") - async def delete( - self, - *, - persistent_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: - """DEPRECATED: Use DELETE /browsers/{id} instead. - - Delete a persistent browser - session by its persistent_id. - - Args: - persistent_id: Persistent browser identifier - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return await self._delete( - "/browsers", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=await async_maybe_transform( - {"persistent_id": persistent_id}, browser_delete_params.BrowserDeleteParams - ), - ), - cast_to=NoneType, - ) - async def curl( self, id: str, @@ -1182,11 +1085,6 @@ def __init__(self, browsers: BrowsersResource) -> None: self.list = to_raw_response_wrapper( browsers.list, ) - self.delete = ( # pyright: ignore[reportDeprecated] - to_raw_response_wrapper( - browsers.delete, # pyright: ignore[reportDeprecated], - ) - ) self.curl = to_raw_response_wrapper( browsers.curl, ) @@ -1248,11 +1146,6 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None: self.list = async_to_raw_response_wrapper( browsers.list, ) - self.delete = ( # pyright: ignore[reportDeprecated] - async_to_raw_response_wrapper( - browsers.delete, # pyright: ignore[reportDeprecated], - ) - ) self.curl = async_to_raw_response_wrapper( browsers.curl, ) @@ -1314,11 +1207,6 @@ def __init__(self, browsers: BrowsersResource) -> None: self.list = to_streamed_response_wrapper( browsers.list, ) - self.delete = ( # pyright: ignore[reportDeprecated] - to_streamed_response_wrapper( - browsers.delete, # pyright: ignore[reportDeprecated], - ) - ) self.curl = to_streamed_response_wrapper( browsers.curl, ) @@ -1380,11 +1268,6 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None: self.list = async_to_streamed_response_wrapper( browsers.list, ) - self.delete = ( # pyright: ignore[reportDeprecated] - async_to_streamed_response_wrapper( - browsers.delete, # pyright: ignore[reportDeprecated], - ) - ) self.curl = async_to_streamed_response_wrapper( browsers.curl, ) diff --git a/src/kernel/resources/projects/limits.py b/src/kernel/resources/projects/limits.py index eeff5930..04ac3148 100644 --- a/src/kernel/resources/projects/limits.py +++ b/src/kernel/resources/projects/limits.py @@ -86,7 +86,6 @@ def update( *, max_concurrent_invocations: Optional[int] | Omit = omit, max_concurrent_sessions: Optional[int] | Omit = omit, - max_persistent_sessions: Optional[int] | Omit = omit, max_pooled_sessions: Optional[int] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -108,9 +107,6 @@ def update( max_concurrent_sessions: Maximum concurrent browser sessions for this project. Set to 0 to remove the cap; omit to leave unchanged. - max_persistent_sessions: Maximum persistent browser sessions for this project. Set to 0 to remove the - cap; omit to leave unchanged. - max_pooled_sessions: Maximum pooled sessions capacity for this project. Set to 0 to remove the cap; omit to leave unchanged. @@ -130,7 +126,6 @@ def update( { "max_concurrent_invocations": max_concurrent_invocations, "max_concurrent_sessions": max_concurrent_sessions, - "max_persistent_sessions": max_persistent_sessions, "max_pooled_sessions": max_pooled_sessions, }, limit_update_params.LimitUpdateParams, @@ -205,7 +200,6 @@ async def update( *, max_concurrent_invocations: Optional[int] | Omit = omit, max_concurrent_sessions: Optional[int] | Omit = omit, - max_persistent_sessions: Optional[int] | Omit = omit, max_pooled_sessions: Optional[int] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -227,9 +221,6 @@ async def update( max_concurrent_sessions: Maximum concurrent browser sessions for this project. Set to 0 to remove the cap; omit to leave unchanged. - max_persistent_sessions: Maximum persistent browser sessions for this project. Set to 0 to remove the - cap; omit to leave unchanged. - max_pooled_sessions: Maximum pooled sessions capacity for this project. Set to 0 to remove the cap; omit to leave unchanged. @@ -249,7 +240,6 @@ async def update( { "max_concurrent_invocations": max_concurrent_invocations, "max_concurrent_sessions": max_concurrent_sessions, - "max_persistent_sessions": max_persistent_sessions, "max_pooled_sessions": max_pooled_sessions, }, limit_update_params.LimitUpdateParams, diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 1a7d19c3..49346e2f 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -26,7 +26,6 @@ from .proxy_check_params import ProxyCheckParams as ProxyCheckParams from .browser_curl_params import BrowserCurlParams as BrowserCurlParams from .browser_list_params import BrowserListParams as BrowserListParams -from .browser_persistence import BrowserPersistence as BrowserPersistence from .credential_provider import CredentialProvider as CredentialProvider from .profile_list_params import ProfileListParams as ProfileListParams from .project_list_params import ProjectListParams as ProjectListParams @@ -35,7 +34,6 @@ from .proxy_check_response import ProxyCheckResponse as ProxyCheckResponse from .browser_create_params import BrowserCreateParams as BrowserCreateParams from .browser_curl_response import BrowserCurlResponse as BrowserCurlResponse -from .browser_delete_params import BrowserDeleteParams as BrowserDeleteParams from .browser_list_response import BrowserListResponse as BrowserListResponse from .browser_update_params import BrowserUpdateParams as BrowserUpdateParams from .profile_create_params import ProfileCreateParams as ProfileCreateParams @@ -63,7 +61,6 @@ from .invocation_follow_params import InvocationFollowParams as InvocationFollowParams from .invocation_list_response import InvocationListResponse as InvocationListResponse from .invocation_update_params import InvocationUpdateParams as InvocationUpdateParams -from .browser_persistence_param import BrowserPersistenceParam as BrowserPersistenceParam from .browser_retrieve_response import BrowserRetrieveResponse as BrowserRetrieveResponse from .extension_upload_response import ExtensionUploadResponse as ExtensionUploadResponse from .browser_pool_create_params import BrowserPoolCreateParams as BrowserPoolCreateParams diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index ca184fc7..1c2ed5e9 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -5,7 +5,6 @@ from typing import Dict, Iterable, Optional from typing_extensions import TypedDict -from .browser_persistence_param import BrowserPersistenceParam from .shared_params.browser_profile import BrowserProfile from .shared_params.browser_viewport import BrowserViewport from .shared_params.browser_extension import BrowserExtension @@ -19,8 +18,7 @@ class BrowserCreateParams(TypedDict, total=False): """Custom Chrome enterprise policy overrides applied to this browser session. Keys are Chrome enterprise policy names; values must match their expected types. - Blocked: kernel-managed policies (extensions, proxy, CDP/automation). Ignored - when reusing an existing persistent session. See + Blocked: kernel-managed policies (extensions, proxy, CDP/automation). See https://chromeenterprise.google/policies/ """ @@ -51,9 +49,6 @@ class BrowserCreateParams(TypedDict, total=False): view. """ - persistence: BrowserPersistenceParam - """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead.""" - profile: BrowserProfile """Profile selection for the browser session. diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index bbb2287e..651216ab 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -7,7 +7,6 @@ from .._models import BaseModel from .browser_usage import BrowserUsage from .browser_pool_ref import BrowserPoolRef -from .browser_persistence import BrowserPersistence from .shared.browser_viewport import BrowserViewport from .browsers.browser_telemetry_config import BrowserTelemetryConfig @@ -64,9 +63,6 @@ class BrowserCreateResponse(BaseModel): kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" - persistence: Optional[BrowserPersistence] = None - """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead.""" - pool: Optional[BrowserPoolRef] = None """Browser pool this session was acquired from, if any.""" diff --git a/src/kernel/types/browser_delete_params.py b/src/kernel/types/browser_delete_params.py deleted file mode 100644 index 4c5b1c6a..00000000 --- a/src/kernel/types/browser_delete_params.py +++ /dev/null @@ -1,12 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Required, TypedDict - -__all__ = ["BrowserDeleteParams"] - - -class BrowserDeleteParams(TypedDict, total=False): - persistent_id: Required[str] - """Persistent browser identifier""" diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index ed14251a..79db0cfc 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -7,7 +7,6 @@ from .._models import BaseModel from .browser_usage import BrowserUsage from .browser_pool_ref import BrowserPoolRef -from .browser_persistence import BrowserPersistence from .shared.browser_viewport import BrowserViewport from .browsers.browser_telemetry_config import BrowserTelemetryConfig @@ -64,9 +63,6 @@ class BrowserListResponse(BaseModel): kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" - persistence: Optional[BrowserPersistence] = None - """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead.""" - pool: Optional[BrowserPoolRef] = None """Browser pool this session was acquired from, if any.""" diff --git a/src/kernel/types/browser_persistence.py b/src/kernel/types/browser_persistence.py deleted file mode 100644 index 381d6306..00000000 --- a/src/kernel/types/browser_persistence.py +++ /dev/null @@ -1,12 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from .._models import BaseModel - -__all__ = ["BrowserPersistence"] - - -class BrowserPersistence(BaseModel): - """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead.""" - - id: str - """DEPRECATED: Unique identifier for the persistent browser session.""" diff --git a/src/kernel/types/browser_persistence_param.py b/src/kernel/types/browser_persistence_param.py deleted file mode 100644 index 6109abfd..00000000 --- a/src/kernel/types/browser_persistence_param.py +++ /dev/null @@ -1,14 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Required, TypedDict - -__all__ = ["BrowserPersistenceParam"] - - -class BrowserPersistenceParam(TypedDict, total=False): - """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead.""" - - id: Required[str] - """DEPRECATED: Unique identifier for the persistent browser session.""" diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py index 20ab1fc5..b3cbabfa 100644 --- a/src/kernel/types/browser_pool_acquire_response.py +++ b/src/kernel/types/browser_pool_acquire_response.py @@ -7,7 +7,6 @@ from .._models import BaseModel from .browser_usage import BrowserUsage from .browser_pool_ref import BrowserPoolRef -from .browser_persistence import BrowserPersistence from .shared.browser_viewport import BrowserViewport from .browsers.browser_telemetry_config import BrowserTelemetryConfig @@ -64,9 +63,6 @@ class BrowserPoolAcquireResponse(BaseModel): kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" - persistence: Optional[BrowserPersistence] = None - """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead.""" - pool: Optional[BrowserPoolRef] = None """Browser pool this session was acquired from, if any.""" diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 6c79b519..86b5fbef 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -7,7 +7,6 @@ from .._models import BaseModel from .browser_usage import BrowserUsage from .browser_pool_ref import BrowserPoolRef -from .browser_persistence import BrowserPersistence from .shared.browser_viewport import BrowserViewport from .browsers.browser_telemetry_config import BrowserTelemetryConfig @@ -64,9 +63,6 @@ class BrowserRetrieveResponse(BaseModel): kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" - persistence: Optional[BrowserPersistence] = None - """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead.""" - pool: Optional[BrowserPoolRef] = None """Browser pool this session was acquired from, if any.""" diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py index 4f237cf5..60e95176 100644 --- a/src/kernel/types/browser_update_response.py +++ b/src/kernel/types/browser_update_response.py @@ -7,7 +7,6 @@ from .._models import BaseModel from .browser_usage import BrowserUsage from .browser_pool_ref import BrowserPoolRef -from .browser_persistence import BrowserPersistence from .shared.browser_viewport import BrowserViewport from .browsers.browser_telemetry_config import BrowserTelemetryConfig @@ -64,9 +63,6 @@ class BrowserUpdateResponse(BaseModel): kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" - persistence: Optional[BrowserPersistence] = None - """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead.""" - pool: Optional[BrowserPoolRef] = None """Browser pool this session was acquired from, if any.""" diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py index 7c60ae22..0a1700c2 100644 --- a/src/kernel/types/invocation_list_browsers_response.py +++ b/src/kernel/types/invocation_list_browsers_response.py @@ -7,7 +7,6 @@ from .._models import BaseModel from .browser_usage import BrowserUsage from .browser_pool_ref import BrowserPoolRef -from .browser_persistence import BrowserPersistence from .shared.browser_viewport import BrowserViewport from .browsers.browser_telemetry_config import BrowserTelemetryConfig @@ -64,9 +63,6 @@ class Browser(BaseModel): kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" - persistence: Optional[BrowserPersistence] = None - """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead.""" - pool: Optional[BrowserPoolRef] = None """Browser pool this session was acquired from, if any.""" diff --git a/src/kernel/types/projects/limit_update_params.py b/src/kernel/types/projects/limit_update_params.py index 6f0ec8ae..387b4c64 100644 --- a/src/kernel/types/projects/limit_update_params.py +++ b/src/kernel/types/projects/limit_update_params.py @@ -21,12 +21,6 @@ class LimitUpdateParams(TypedDict, total=False): Set to 0 to remove the cap; omit to leave unchanged. """ - max_persistent_sessions: Optional[int] - """Maximum persistent browser sessions for this project. - - Set to 0 to remove the cap; omit to leave unchanged. - """ - max_pooled_sessions: Optional[int] """Maximum pooled sessions capacity for this project. diff --git a/src/kernel/types/projects/project_limits.py b/src/kernel/types/projects/project_limits.py index bf49b2a2..99868fc8 100644 --- a/src/kernel/types/projects/project_limits.py +++ b/src/kernel/types/projects/project_limits.py @@ -20,12 +20,6 @@ class ProjectLimits(BaseModel): Null means no project-level cap. """ - max_persistent_sessions: Optional[int] = None - """Maximum persistent browser sessions for this project. - - Null means no project-level cap. - """ - max_pooled_sessions: Optional[int] = None """Maximum pooled sessions capacity for this project. diff --git a/tests/api_resources/projects/test_limits.py b/tests/api_resources/projects/test_limits.py index 9df6a0df..a42a0bde 100644 --- a/tests/api_resources/projects/test_limits.py +++ b/tests/api_resources/projects/test_limits.py @@ -74,7 +74,6 @@ def test_method_update_with_all_params(self, client: Kernel) -> None: id="id", max_concurrent_invocations=0, max_concurrent_sessions=0, - max_persistent_sessions=0, max_pooled_sessions=0, ) assert_matches_type(ProjectLimits, limit, path=["response"]) @@ -176,7 +175,6 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> id="id", max_concurrent_invocations=0, max_concurrent_sessions=0, - max_persistent_sessions=0, max_pooled_sessions=0, ) assert_matches_type(ProjectLimits, limit, path=["response"]) diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index dcd20977..57aa89fe 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -18,8 +18,6 @@ ) from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination -# pyright: reportDeprecated=false - base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -47,7 +45,6 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: headless=False, invocation_id="rr33xuugxj9h0bkf1rdt2bet", kiosk_mode=True, - persistence={"id": "my-awesome-browser-for-user-1234"}, profile={ "id": "id", "name": "name", @@ -257,44 +254,6 @@ def test_streaming_response_list(self, client: Kernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_delete(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - browser = client.browsers.delete( - persistent_id="persistent_id", - ) - - assert browser is None - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_delete(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - response = client.browsers.with_raw_response.delete( - persistent_id="persistent_id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - browser = response.parse() - assert browser is None - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_delete(self, client: Kernel) -> None: - with pytest.warns(DeprecationWarning): - with client.browsers.with_streaming_response.delete( - persistent_id="persistent_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - browser = response.parse() - assert browser is None - - assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_curl(self, client: Kernel) -> None: @@ -490,7 +449,6 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> headless=False, invocation_id="rr33xuugxj9h0bkf1rdt2bet", kiosk_mode=True, - persistence={"id": "my-awesome-browser-for-user-1234"}, profile={ "id": "id", "name": "name", @@ -700,44 +658,6 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_delete(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - browser = await async_client.browsers.delete( - persistent_id="persistent_id", - ) - - assert browser is None - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - response = await async_client.browsers.with_raw_response.delete( - persistent_id="persistent_id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - browser = await response.parse() - assert browser is None - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None: - with pytest.warns(DeprecationWarning): - async with async_client.browsers.with_streaming_response.delete( - persistent_id="persistent_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - browser = await response.parse() - assert browser is None - - assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_curl(self, async_client: AsyncKernel) -> None: From 5d165d8e09ef366cb5c80a7ced1c38ec053d9fb5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 19:17:13 +0000 Subject: [PATCH 403/448] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 6007fff2..6f5d31f0 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-346b1affebf16e7f943bcf4db307a5589fcae636c9147dd0afbe5c02cf12ad30.yml -openapi_spec_hash: 348df436759d220264f12450919211c3 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-5d96fec7a84722f300bd99db7352d6284141826f3412f6d370ac0926edf03d42.yml +openapi_spec_hash: d4e1a29ac06f9543e0ef69372eb3ff35 config_hash: ae3dea7997fb5d36fa41979f9585ed78 From 34a60cc109a665f503c495f5f9532782c071b55e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 19:58:19 +0000 Subject: [PATCH 404/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 87d3d84c..2afb750c 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.56.0" + ".": "0.57.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index c98902d7..6956b281 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.56.0" +version = "0.57.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index a4d2a572..fb252c57 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.56.0" # x-release-please-version +__version__ = "0.57.0" # x-release-please-version From 40d2a6bffccdc688887980509a4adb9aedee4ec9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 24 May 2026 21:26:08 +0000 Subject: [PATCH 405/448] feat: api: dual-route /projects under /org/projects, deprecate /projects --- .stats.yml | 6 +++--- api.md | 14 +++++++------- src/kernel/resources/projects/limits.py | 8 ++++---- src/kernel/resources/projects/projects.py | 20 ++++++++++---------- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/.stats.yml b/.stats.yml index 6f5d31f0..dbf097fb 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-5d96fec7a84722f300bd99db7352d6284141826f3412f6d370ac0926edf03d42.yml -openapi_spec_hash: d4e1a29ac06f9543e0ef69372eb3ff35 -config_hash: ae3dea7997fb5d36fa41979f9585ed78 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-a0ad160eb0fb11e201e240de8487c12992a474076aeed1c1167ee54ade43edfb.yml +openapi_spec_hash: 84a7075fddbe17b1446a759e8cc39047 +config_hash: 26beac3050665664d5d74d2bbfe9e808 diff --git a/api.md b/api.md index b652f86d..4785c2d9 100644 --- a/api.md +++ b/api.md @@ -395,11 +395,11 @@ from kernel.types import CreateProjectRequest, Project, UpdateProjectRequest Methods: -- client.projects.create(\*\*params) -> Project -- client.projects.retrieve(id) -> Project -- client.projects.update(id, \*\*params) -> Project -- client.projects.list(\*\*params) -> SyncOffsetPagination[Project] -- client.projects.delete(id) -> None +- client.projects.create(\*\*params) -> Project +- client.projects.retrieve(id) -> Project +- client.projects.update(id, \*\*params) -> Project +- client.projects.list(\*\*params) -> SyncOffsetPagination[Project] +- client.projects.delete(id) -> None ## Limits @@ -411,8 +411,8 @@ from kernel.types.projects import ProjectLimits, UpdateProjectLimitsRequest Methods: -- client.projects.limits.retrieve(id) -> ProjectLimits -- client.projects.limits.update(id, \*\*params) -> ProjectLimits +- client.projects.limits.retrieve(id) -> ProjectLimits +- client.projects.limits.update(id, \*\*params) -> ProjectLimits # CredentialProviders diff --git a/src/kernel/resources/projects/limits.py b/src/kernel/resources/projects/limits.py index 04ac3148..ade9302c 100644 --- a/src/kernel/resources/projects/limits.py +++ b/src/kernel/resources/projects/limits.py @@ -73,7 +73,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - path_template("/projects/{id}/limits", id=id), + path_template("/org/projects/{id}/limits", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -121,7 +121,7 @@ def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._patch( - path_template("/projects/{id}/limits", id=id), + path_template("/org/projects/{id}/limits", id=id), body=maybe_transform( { "max_concurrent_invocations": max_concurrent_invocations, @@ -187,7 +187,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - path_template("/projects/{id}/limits", id=id), + path_template("/org/projects/{id}/limits", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -235,7 +235,7 @@ async def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._patch( - path_template("/projects/{id}/limits", id=id), + path_template("/org/projects/{id}/limits", id=id), body=await async_maybe_transform( { "max_concurrent_invocations": max_concurrent_invocations, diff --git a/src/kernel/resources/projects/projects.py b/src/kernel/resources/projects/projects.py index 87fae1b8..ea977689 100644 --- a/src/kernel/resources/projects/projects.py +++ b/src/kernel/resources/projects/projects.py @@ -85,7 +85,7 @@ def create( timeout: Override the client-level default timeout for this request, in seconds """ return self._post( - "/projects", + "/org/projects", body=maybe_transform({"name": name}, project_create_params.ProjectCreateParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -119,7 +119,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - path_template("/projects/{id}", id=id), + path_template("/org/projects/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -158,7 +158,7 @@ def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._patch( - path_template("/projects/{id}", id=id), + path_template("/org/projects/{id}", id=id), body=maybe_transform( { "name": name, @@ -204,7 +204,7 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ return self._get_api_list( - "/projects", + "/org/projects", page=SyncOffsetPagination[Project], options=make_request_options( extra_headers=extra_headers, @@ -251,7 +251,7 @@ def delete( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( - path_template("/projects/{id}", id=id), + path_template("/org/projects/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -312,7 +312,7 @@ async def create( timeout: Override the client-level default timeout for this request, in seconds """ return await self._post( - "/projects", + "/org/projects", body=await async_maybe_transform({"name": name}, project_create_params.ProjectCreateParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -346,7 +346,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - path_template("/projects/{id}", id=id), + path_template("/org/projects/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -385,7 +385,7 @@ async def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._patch( - path_template("/projects/{id}", id=id), + path_template("/org/projects/{id}", id=id), body=await async_maybe_transform( { "name": name, @@ -431,7 +431,7 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ return self._get_api_list( - "/projects", + "/org/projects", page=AsyncOffsetPagination[Project], options=make_request_options( extra_headers=extra_headers, @@ -478,7 +478,7 @@ async def delete( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( - path_template("/projects/{id}", id=id), + path_template("/org/projects/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), From f797100609171b312f9ef0604eaf5d9048b4f439 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 26 May 2026 17:42:46 +0000 Subject: [PATCH 406/448] feat: Support telemetry enabled request config --- .stats.yml | 6 +-- api.md | 1 + src/kernel/resources/browsers/browsers.py | 40 +++++++++++-------- src/kernel/types/browser_create_params.py | 10 +++-- src/kernel/types/browser_update_params.py | 11 ++--- src/kernel/types/browsers/__init__.py | 4 +- .../browsers/browser_telemetry_config.py | 4 +- .../browser_telemetry_config_param.py | 16 -------- .../browser_telemetry_request_config_param.py | 28 +++++++++++++ tests/api_resources/test_browsers.py | 12 ++++-- 10 files changed, 80 insertions(+), 52 deletions(-) delete mode 100644 src/kernel/types/browsers/browser_telemetry_config_param.py create mode 100644 src/kernel/types/browsers/browser_telemetry_request_config_param.py diff --git a/.stats.yml b/.stats.yml index dbf097fb..47ab7c16 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-a0ad160eb0fb11e201e240de8487c12992a474076aeed1c1167ee54ade43edfb.yml -openapi_spec_hash: 84a7075fddbe17b1446a759e8cc39047 -config_hash: 26beac3050665664d5d74d2bbfe9e808 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e9c99662d29710f105847d461f8919e06f6aa2e43b0e1a6285d0b137643a7907.yml +openapi_spec_hash: 4415cb4790c7a5ec892f4e3521217cb4 +config_hash: 27b38657d9a3b33328be930eeb319628 diff --git a/api.md b/api.md index 4785c2d9..106e3cd8 100644 --- a/api.md +++ b/api.md @@ -137,6 +137,7 @@ from kernel.types.browsers import ( BrowserTelemetryCategoryConfig, BrowserTelemetryConfig, BrowserTelemetryEvent, + BrowserTelemetryRequestConfig, TelemetryStreamResponse, ) ``` diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 5d10f7f9..503a9239 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -92,7 +92,7 @@ from ...types.shared_params.browser_profile import BrowserProfile from ...types.shared_params.browser_viewport import BrowserViewport from ...types.shared_params.browser_extension import BrowserExtension -from ...types.browsers.browser_telemetry_config_param import BrowserTelemetryConfigParam +from ...types.browsers.browser_telemetry_request_config_param import BrowserTelemetryRequestConfigParam __all__ = ["BrowsersResource", "AsyncBrowsersResource"] @@ -166,7 +166,7 @@ def create( proxy_id: str | Omit = omit, start_url: str | Omit = omit, stealth: bool | Omit = omit, - telemetry: Optional[BrowserTelemetryConfigParam] | Omit = omit, + telemetry: Optional[BrowserTelemetryRequestConfigParam] | Omit = omit, timeout_seconds: int | Omit = omit, viewport: BrowserViewport | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -212,9 +212,11 @@ def create( stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. - telemetry: Telemetry configuration for the browser session. If provided, telemetry capture - starts with the specified category filter when the session is created. If - omitted, no telemetry capture is started. + telemetry: Telemetry configuration for the browser session. Set enabled to true to start + capture using VM defaults, or provide browser category settings. If omitted, + null, set to an empty object ({}), set to enabled: false without browser + category settings, or all four categories are explicitly disabled, capture is + not started. timeout_seconds: The number of seconds of inactivity before the browser session is terminated. Activity includes CDP connections and live view connections. Defaults to 60 @@ -318,7 +320,7 @@ def update( disable_default_proxy: bool | Omit = omit, profile: BrowserProfile | Omit = omit, proxy_id: Optional[str] | Omit = omit, - telemetry: Optional[BrowserTelemetryConfigParam] | Omit = omit, + telemetry: Optional[BrowserTelemetryRequestConfigParam] | Omit = omit, viewport: browser_update_params.Viewport | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -341,9 +343,10 @@ def update( proxy. telemetry: Telemetry configuration. Omit, set to null, or set to an empty object ({}) to - leave the existing configuration unchanged (no-op). To enable capture for all - categories using VM defaults, set browser to an empty object ({"browser": {}}). - To stop capture, set every category's enabled to false. + leave the existing configuration unchanged. Set enabled to true to enable + capture using VM defaults. Set enabled to false to stop capture. Provide browser + category settings for per-category updates. Explicitly disabling all four + categories also stops capture. viewport: Viewport configuration to apply to the browser session. @@ -652,7 +655,7 @@ async def create( proxy_id: str | Omit = omit, start_url: str | Omit = omit, stealth: bool | Omit = omit, - telemetry: Optional[BrowserTelemetryConfigParam] | Omit = omit, + telemetry: Optional[BrowserTelemetryRequestConfigParam] | Omit = omit, timeout_seconds: int | Omit = omit, viewport: BrowserViewport | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -698,9 +701,11 @@ async def create( stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. - telemetry: Telemetry configuration for the browser session. If provided, telemetry capture - starts with the specified category filter when the session is created. If - omitted, no telemetry capture is started. + telemetry: Telemetry configuration for the browser session. Set enabled to true to start + capture using VM defaults, or provide browser category settings. If omitted, + null, set to an empty object ({}), set to enabled: false without browser + category settings, or all four categories are explicitly disabled, capture is + not started. timeout_seconds: The number of seconds of inactivity before the browser session is terminated. Activity includes CDP connections and live view connections. Defaults to 60 @@ -804,7 +809,7 @@ async def update( disable_default_proxy: bool | Omit = omit, profile: BrowserProfile | Omit = omit, proxy_id: Optional[str] | Omit = omit, - telemetry: Optional[BrowserTelemetryConfigParam] | Omit = omit, + telemetry: Optional[BrowserTelemetryRequestConfigParam] | Omit = omit, viewport: browser_update_params.Viewport | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -827,9 +832,10 @@ async def update( proxy. telemetry: Telemetry configuration. Omit, set to null, or set to an empty object ({}) to - leave the existing configuration unchanged (no-op). To enable capture for all - categories using VM defaults, set browser to an empty object ({"browser": {}}). - To stop capture, set every category's enabled to false. + leave the existing configuration unchanged. Set enabled to true to enable + capture using VM defaults. Set enabled to false to stop capture. Provide browser + category settings for per-category updates. Explicitly disabling all four + categories also stops capture. viewport: Viewport configuration to apply to the browser session. diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index 1c2ed5e9..82406a5a 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -8,7 +8,7 @@ from .shared_params.browser_profile import BrowserProfile from .shared_params.browser_viewport import BrowserViewport from .shared_params.browser_extension import BrowserExtension -from .browsers.browser_telemetry_config_param import BrowserTelemetryConfigParam +from .browsers.browser_telemetry_request_config_param import BrowserTelemetryRequestConfigParam __all__ = ["BrowserCreateParams"] @@ -75,11 +75,13 @@ class BrowserCreateParams(TypedDict, total=False): mechanisms. """ - telemetry: Optional[BrowserTelemetryConfigParam] + telemetry: Optional[BrowserTelemetryRequestConfigParam] """Telemetry configuration for the browser session. - If provided, telemetry capture starts with the specified category filter when - the session is created. If omitted, no telemetry capture is started. + Set enabled to true to start capture using VM defaults, or provide browser + category settings. If omitted, null, set to an empty object ({}), set to + enabled: false without browser category settings, or all four categories are + explicitly disabled, capture is not started. """ timeout_seconds: int diff --git a/src/kernel/types/browser_update_params.py b/src/kernel/types/browser_update_params.py index 297d0388..c883089d 100644 --- a/src/kernel/types/browser_update_params.py +++ b/src/kernel/types/browser_update_params.py @@ -7,7 +7,7 @@ from .shared_params.browser_profile import BrowserProfile from .shared_params.browser_viewport import BrowserViewport -from .browsers.browser_telemetry_config_param import BrowserTelemetryConfigParam +from .browsers.browser_telemetry_request_config_param import BrowserTelemetryRequestConfigParam __all__ = ["BrowserUpdateParams", "Viewport"] @@ -31,13 +31,14 @@ class BrowserUpdateParams(TypedDict, total=False): Omit to leave unchanged, set to empty string to remove proxy. """ - telemetry: Optional[BrowserTelemetryConfigParam] + telemetry: Optional[BrowserTelemetryRequestConfigParam] """Telemetry configuration. Omit, set to null, or set to an empty object ({}) to leave the existing - configuration unchanged (no-op). To enable capture for all categories using VM - defaults, set browser to an empty object ({"browser": {}}). To stop capture, set - every category's enabled to false. + configuration unchanged. Set enabled to true to enable capture using VM + defaults. Set enabled to false to stop capture. Provide browser category + settings for per-category updates. Explicitly disabling all four categories also + stops capture. """ viewport: Viewport diff --git a/src/kernel/types/browsers/__init__.py b/src/kernel/types/browsers/__init__.py index a3bca412..619a59c4 100644 --- a/src/kernel/types/browsers/__init__.py +++ b/src/kernel/types/browsers/__init__.py @@ -57,7 +57,6 @@ from .browser_page_tab_opened_event import BrowserPageTabOpenedEvent as BrowserPageTabOpenedEvent from .f_set_file_permissions_params import FSetFilePermissionsParams as FSetFilePermissionsParams from .browser_network_response_event import BrowserNetworkResponseEvent as BrowserNetworkResponseEvent -from .browser_telemetry_config_param import BrowserTelemetryConfigParam as BrowserTelemetryConfigParam from .process_stdout_stream_response import ProcessStdoutStreamResponse as ProcessStdoutStreamResponse from .browser_interaction_click_event import BrowserInteractionClickEvent as BrowserInteractionClickEvent from .browser_page_layout_shift_event import BrowserPageLayoutShiftEvent as BrowserPageLayoutShiftEvent @@ -83,6 +82,9 @@ from .browser_monitor_reconnect_failed_event import ( BrowserMonitorReconnectFailedEvent as BrowserMonitorReconnectFailedEvent, ) +from .browser_telemetry_request_config_param import ( + BrowserTelemetryRequestConfigParam as BrowserTelemetryRequestConfigParam, +) from .browser_telemetry_category_config_param import ( BrowserTelemetryCategoryConfigParam as BrowserTelemetryCategoryConfigParam, ) diff --git a/src/kernel/types/browsers/browser_telemetry_config.py b/src/kernel/types/browsers/browser_telemetry_config.py index d4365eeb..2fae7a3d 100644 --- a/src/kernel/types/browsers/browser_telemetry_config.py +++ b/src/kernel/types/browsers/browser_telemetry_config.py @@ -9,7 +9,7 @@ class BrowserTelemetryConfig(BaseModel): - """Telemetry configuration for a browser session.""" + """Active telemetry configuration for a browser session.""" browser: Optional[BrowserTelemetryCategoriesConfig] = None - """Per-category enable/disable flags. If omitted, all categories are captured.""" + """Per-category enable/disable flags.""" diff --git a/src/kernel/types/browsers/browser_telemetry_config_param.py b/src/kernel/types/browsers/browser_telemetry_config_param.py deleted file mode 100644 index 33464340..00000000 --- a/src/kernel/types/browsers/browser_telemetry_config_param.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 TypedDict - -from .browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam - -__all__ = ["BrowserTelemetryConfigParam"] - - -class BrowserTelemetryConfigParam(TypedDict, total=False): - """Telemetry configuration for a browser session.""" - - browser: BrowserTelemetryCategoriesConfigParam - """Per-category enable/disable flags. If omitted, all categories are captured.""" diff --git a/src/kernel/types/browsers/browser_telemetry_request_config_param.py b/src/kernel/types/browsers/browser_telemetry_request_config_param.py new file mode 100644 index 00000000..d901a74e --- /dev/null +++ b/src/kernel/types/browsers/browser_telemetry_request_config_param.py @@ -0,0 +1,28 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +from .browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam + +__all__ = ["BrowserTelemetryRequestConfigParam"] + + +class BrowserTelemetryRequestConfigParam(TypedDict, total=False): + """Telemetry request configuration for a browser session.""" + + browser: BrowserTelemetryCategoriesConfigParam + """Per-category enable/disable flags. + + If enabled is true and browser is omitted or empty, the VM default category set + is used. Explicitly disabling all four categories stops capture on update and + starts no capture on create. + """ + + enabled: bool + """Request shortcut for browser telemetry capture. + + True enables capture using VM defaults. False stops capture on update and starts + no capture on create. Cannot be combined with browser category settings. + """ diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index 57aa89fe..c845e52b 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -59,7 +59,8 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: "interaction": {"enabled": True}, "network": {"enabled": True}, "page": {"enabled": True}, - } + }, + "enabled": True, }, timeout_seconds=10, viewport={ @@ -169,7 +170,8 @@ def test_method_update_with_all_params(self, client: Kernel) -> None: "interaction": {"enabled": True}, "network": {"enabled": True}, "page": {"enabled": True}, - } + }, + "enabled": True, }, viewport={ "height": 800, @@ -463,7 +465,8 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> "interaction": {"enabled": True}, "network": {"enabled": True}, "page": {"enabled": True}, - } + }, + "enabled": True, }, timeout_seconds=10, viewport={ @@ -573,7 +576,8 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> "interaction": {"enabled": True}, "network": {"enabled": True}, "page": {"enabled": True}, - } + }, + "enabled": True, }, viewport={ "height": 800, From 943da2dfdb683d9e201a298b0c8782869e1b51b9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 26 May 2026 18:33:59 +0000 Subject: [PATCH 407/448] feat: Support telemetry enabled request config and fix SDK metadata --- .stats.yml | 6 ++-- api.md | 1 - src/kernel/resources/browsers/browsers.py | 9 +++--- src/kernel/types/browser_create_params.py | 29 +++++++++++++++++-- src/kernel/types/browser_update_params.py | 29 +++++++++++++++++-- src/kernel/types/browsers/__init__.py | 3 -- .../browser_telemetry_request_config_param.py | 28 ------------------ 7 files changed, 59 insertions(+), 46 deletions(-) delete mode 100644 src/kernel/types/browsers/browser_telemetry_request_config_param.py diff --git a/.stats.yml b/.stats.yml index 47ab7c16..7fdb7bfb 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e9c99662d29710f105847d461f8919e06f6aa2e43b0e1a6285d0b137643a7907.yml -openapi_spec_hash: 4415cb4790c7a5ec892f4e3521217cb4 -config_hash: 27b38657d9a3b33328be930eeb319628 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-0a186c486b56f555cab374ea5f2adbef2d718b5c9190a48c862f0fdf1232324f.yml +openapi_spec_hash: fad386b8e8712e6639ed9689e9dfc070 +config_hash: 5dde8b5de321a7bb96f695a69eb21c23 diff --git a/api.md b/api.md index 106e3cd8..4785c2d9 100644 --- a/api.md +++ b/api.md @@ -137,7 +137,6 @@ from kernel.types.browsers import ( BrowserTelemetryCategoryConfig, BrowserTelemetryConfig, BrowserTelemetryEvent, - BrowserTelemetryRequestConfig, TelemetryStreamResponse, ) ``` diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 503a9239..3644c019 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -92,7 +92,6 @@ from ...types.shared_params.browser_profile import BrowserProfile from ...types.shared_params.browser_viewport import BrowserViewport from ...types.shared_params.browser_extension import BrowserExtension -from ...types.browsers.browser_telemetry_request_config_param import BrowserTelemetryRequestConfigParam __all__ = ["BrowsersResource", "AsyncBrowsersResource"] @@ -166,7 +165,7 @@ def create( proxy_id: str | Omit = omit, start_url: str | Omit = omit, stealth: bool | Omit = omit, - telemetry: Optional[BrowserTelemetryRequestConfigParam] | Omit = omit, + telemetry: Optional[browser_create_params.Telemetry] | Omit = omit, timeout_seconds: int | Omit = omit, viewport: BrowserViewport | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -320,7 +319,7 @@ def update( disable_default_proxy: bool | Omit = omit, profile: BrowserProfile | Omit = omit, proxy_id: Optional[str] | Omit = omit, - telemetry: Optional[BrowserTelemetryRequestConfigParam] | Omit = omit, + telemetry: Optional[browser_update_params.Telemetry] | Omit = omit, viewport: browser_update_params.Viewport | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -655,7 +654,7 @@ async def create( proxy_id: str | Omit = omit, start_url: str | Omit = omit, stealth: bool | Omit = omit, - telemetry: Optional[BrowserTelemetryRequestConfigParam] | Omit = omit, + telemetry: Optional[browser_create_params.Telemetry] | Omit = omit, timeout_seconds: int | Omit = omit, viewport: BrowserViewport | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -809,7 +808,7 @@ async def update( disable_default_proxy: bool | Omit = omit, profile: BrowserProfile | Omit = omit, proxy_id: Optional[str] | Omit = omit, - telemetry: Optional[BrowserTelemetryRequestConfigParam] | Omit = omit, + telemetry: Optional[browser_update_params.Telemetry] | Omit = omit, viewport: browser_update_params.Viewport | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index 82406a5a..c94071cf 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -8,9 +8,9 @@ from .shared_params.browser_profile import BrowserProfile from .shared_params.browser_viewport import BrowserViewport from .shared_params.browser_extension import BrowserExtension -from .browsers.browser_telemetry_request_config_param import BrowserTelemetryRequestConfigParam +from .browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam -__all__ = ["BrowserCreateParams"] +__all__ = ["BrowserCreateParams", "Telemetry"] class BrowserCreateParams(TypedDict, total=False): @@ -75,7 +75,7 @@ class BrowserCreateParams(TypedDict, total=False): mechanisms. """ - telemetry: Optional[BrowserTelemetryRequestConfigParam] + telemetry: Optional[Telemetry] """Telemetry configuration for the browser session. Set enabled to true to start capture using VM defaults, or provide browser @@ -108,3 +108,26 @@ class BrowserCreateParams(TypedDict, total=False): based on the resolution (higher resolutions use lower refresh rates to keep bandwidth reasonable). """ + + +class Telemetry(TypedDict, total=False): + """Telemetry configuration for the browser session. + + Set enabled to true to start capture using VM defaults, or provide browser category settings. If omitted, null, set to an empty object ({}), set to enabled: false without browser category settings, or all four categories are explicitly disabled, capture is not started. + """ + + browser: BrowserTelemetryCategoriesConfigParam + """Per-category enable/disable flags. + + If enabled is true and browser is omitted or empty, the VM default category set + is used. Explicitly disabling all four categories stops capture on update and + starts no capture on create. + """ + + enabled: bool + """Request shortcut for browser telemetry capture. + + True enables capture using VM defaults unless browser category settings are + provided. False stops capture on update and starts no capture on create. + enabled=false cannot be combined with browser category settings. + """ diff --git a/src/kernel/types/browser_update_params.py b/src/kernel/types/browser_update_params.py index c883089d..837c1784 100644 --- a/src/kernel/types/browser_update_params.py +++ b/src/kernel/types/browser_update_params.py @@ -7,9 +7,9 @@ from .shared_params.browser_profile import BrowserProfile from .shared_params.browser_viewport import BrowserViewport -from .browsers.browser_telemetry_request_config_param import BrowserTelemetryRequestConfigParam +from .browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam -__all__ = ["BrowserUpdateParams", "Viewport"] +__all__ = ["BrowserUpdateParams", "Telemetry", "Viewport"] class BrowserUpdateParams(TypedDict, total=False): @@ -31,7 +31,7 @@ class BrowserUpdateParams(TypedDict, total=False): Omit to leave unchanged, set to empty string to remove proxy. """ - telemetry: Optional[BrowserTelemetryRequestConfigParam] + telemetry: Optional[Telemetry] """Telemetry configuration. Omit, set to null, or set to an empty object ({}) to leave the existing @@ -45,6 +45,29 @@ class BrowserUpdateParams(TypedDict, total=False): """Viewport configuration to apply to the browser session.""" +class Telemetry(TypedDict, total=False): + """Telemetry configuration. + + Omit, set to null, or set to an empty object ({}) to leave the existing configuration unchanged. Set enabled to true to enable capture using VM defaults. Set enabled to false to stop capture. Provide browser category settings for per-category updates. Explicitly disabling all four categories also stops capture. + """ + + browser: BrowserTelemetryCategoriesConfigParam + """Per-category enable/disable flags. + + If enabled is true and browser is omitted or empty, the VM default category set + is used. Explicitly disabling all four categories stops capture on update and + starts no capture on create. + """ + + enabled: bool + """Request shortcut for browser telemetry capture. + + True enables capture using VM defaults unless browser category settings are + provided. False stops capture on update and starts no capture on create. + enabled=false cannot be combined with browser category settings. + """ + + class Viewport(BrowserViewport, total=False): """Viewport configuration to apply to the browser session.""" diff --git a/src/kernel/types/browsers/__init__.py b/src/kernel/types/browsers/__init__.py index 619a59c4..34f4fd64 100644 --- a/src/kernel/types/browsers/__init__.py +++ b/src/kernel/types/browsers/__init__.py @@ -82,9 +82,6 @@ from .browser_monitor_reconnect_failed_event import ( BrowserMonitorReconnectFailedEvent as BrowserMonitorReconnectFailedEvent, ) -from .browser_telemetry_request_config_param import ( - BrowserTelemetryRequestConfigParam as BrowserTelemetryRequestConfigParam, -) from .browser_telemetry_category_config_param import ( BrowserTelemetryCategoryConfigParam as BrowserTelemetryCategoryConfigParam, ) diff --git a/src/kernel/types/browsers/browser_telemetry_request_config_param.py b/src/kernel/types/browsers/browser_telemetry_request_config_param.py deleted file mode 100644 index d901a74e..00000000 --- a/src/kernel/types/browsers/browser_telemetry_request_config_param.py +++ /dev/null @@ -1,28 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import TypedDict - -from .browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam - -__all__ = ["BrowserTelemetryRequestConfigParam"] - - -class BrowserTelemetryRequestConfigParam(TypedDict, total=False): - """Telemetry request configuration for a browser session.""" - - browser: BrowserTelemetryCategoriesConfigParam - """Per-category enable/disable flags. - - If enabled is true and browser is omitted or empty, the VM default category set - is used. Explicitly disabling all four categories stops capture on update and - starts no capture on create. - """ - - enabled: bool - """Request shortcut for browser telemetry capture. - - True enables capture using VM defaults. False stops capture on update and starts - no capture on create. Cannot be combined with browser category settings. - """ From 10304ade31aa5710dac2c4e6a9cfc2d0bafe714e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 27 May 2026 15:07:50 +0000 Subject: [PATCH 408/448] feat: [codex] Expose API keys in SDK config --- .stats.yml | 6 +- api.md | 16 + src/kernel/_client.py | 44 ++ src/kernel/resources/__init__.py | 14 + src/kernel/resources/api_keys.py | 557 ++++++++++++++++++++++ src/kernel/types/__init__.py | 5 + src/kernel/types/api_key.py | 47 ++ src/kernel/types/api_key_create_params.py | 19 + src/kernel/types/api_key_list_params.py | 15 + src/kernel/types/api_key_update_params.py | 12 + src/kernel/types/created_api_key.py | 12 + tests/api_resources/test_api_keys.py | 447 +++++++++++++++++ 12 files changed, 1191 insertions(+), 3 deletions(-) create mode 100644 src/kernel/resources/api_keys.py create mode 100644 src/kernel/types/api_key.py create mode 100644 src/kernel/types/api_key_create_params.py create mode 100644 src/kernel/types/api_key_list_params.py create mode 100644 src/kernel/types/api_key_update_params.py create mode 100644 src/kernel/types/created_api_key.py create mode 100644 tests/api_resources/test_api_keys.py diff --git a/.stats.yml b/.stats.yml index 7fdb7bfb..e86e3342 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-0a186c486b56f555cab374ea5f2adbef2d718b5c9190a48c862f0fdf1232324f.yml +configured_endpoints: 117 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-54d9016df9730e65881df9f694df4f33620b0c032f177849d36ccc426cd42fa8.yml openapi_spec_hash: fad386b8e8712e6639ed9689e9dfc070 -config_hash: 5dde8b5de321a7bb96f695a69eb21c23 +config_hash: 58396d6c1fafaf72b26596b9117edf48 diff --git a/api.md b/api.md index 4785c2d9..1dc6fece 100644 --- a/api.md +++ b/api.md @@ -414,6 +414,22 @@ Methods: - client.projects.limits.retrieve(id) -> ProjectLimits - client.projects.limits.update(id, \*\*params) -> ProjectLimits +# APIKeys + +Types: + +```python +from kernel.types import APIKey, CreateAPIKeyRequest, CreatedAPIKey, UpdateAPIKeyRequest +``` + +Methods: + +- client.api_keys.create(\*\*params) -> CreatedAPIKey +- client.api_keys.retrieve(id) -> APIKey +- client.api_keys.update(id, \*\*params) -> APIKey +- client.api_keys.list(\*\*params) -> SyncOffsetPagination[APIKey] +- client.api_keys.delete(id) -> None + # CredentialProviders Types: diff --git a/src/kernel/_client.py b/src/kernel/_client.py index c2c7f4b8..7e627454 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -39,6 +39,7 @@ apps, auth, proxies, + api_keys, browsers, profiles, projects, @@ -51,6 +52,7 @@ ) from .resources.apps import AppsResource, AsyncAppsResource from .resources.proxies import ProxiesResource, AsyncProxiesResource + from .resources.api_keys import APIKeysResource, AsyncAPIKeysResource from .resources.profiles import ProfilesResource, AsyncProfilesResource from .resources.auth.auth import AuthResource, AsyncAuthResource from .resources.extensions import ExtensionsResource, AsyncExtensionsResource @@ -244,6 +246,13 @@ def projects(self) -> ProjectsResource: return ProjectsResource(self) + @cached_property + def api_keys(self) -> APIKeysResource: + """Create and manage API keys for organization and project-scoped access.""" + from .resources.api_keys import APIKeysResource + + return APIKeysResource(self) + @cached_property def credential_providers(self) -> CredentialProvidersResource: """Configure external credential providers like 1Password.""" @@ -530,6 +539,13 @@ def projects(self) -> AsyncProjectsResource: return AsyncProjectsResource(self) + @cached_property + def api_keys(self) -> AsyncAPIKeysResource: + """Create and manage API keys for organization and project-scoped access.""" + from .resources.api_keys import AsyncAPIKeysResource + + return AsyncAPIKeysResource(self) + @cached_property def credential_providers(self) -> AsyncCredentialProvidersResource: """Configure external credential providers like 1Password.""" @@ -734,6 +750,13 @@ def projects(self) -> projects.ProjectsResourceWithRawResponse: return ProjectsResourceWithRawResponse(self._client.projects) + @cached_property + def api_keys(self) -> api_keys.APIKeysResourceWithRawResponse: + """Create and manage API keys for organization and project-scoped access.""" + from .resources.api_keys import APIKeysResourceWithRawResponse + + return APIKeysResourceWithRawResponse(self._client.api_keys) + @cached_property def credential_providers(self) -> credential_providers.CredentialProvidersResourceWithRawResponse: """Configure external credential providers like 1Password.""" @@ -824,6 +847,13 @@ def projects(self) -> projects.AsyncProjectsResourceWithRawResponse: return AsyncProjectsResourceWithRawResponse(self._client.projects) + @cached_property + def api_keys(self) -> api_keys.AsyncAPIKeysResourceWithRawResponse: + """Create and manage API keys for organization and project-scoped access.""" + from .resources.api_keys import AsyncAPIKeysResourceWithRawResponse + + return AsyncAPIKeysResourceWithRawResponse(self._client.api_keys) + @cached_property def credential_providers(self) -> credential_providers.AsyncCredentialProvidersResourceWithRawResponse: """Configure external credential providers like 1Password.""" @@ -914,6 +944,13 @@ def projects(self) -> projects.ProjectsResourceWithStreamingResponse: return ProjectsResourceWithStreamingResponse(self._client.projects) + @cached_property + def api_keys(self) -> api_keys.APIKeysResourceWithStreamingResponse: + """Create and manage API keys for organization and project-scoped access.""" + from .resources.api_keys import APIKeysResourceWithStreamingResponse + + return APIKeysResourceWithStreamingResponse(self._client.api_keys) + @cached_property def credential_providers(self) -> credential_providers.CredentialProvidersResourceWithStreamingResponse: """Configure external credential providers like 1Password.""" @@ -1004,6 +1041,13 @@ def projects(self) -> projects.AsyncProjectsResourceWithStreamingResponse: return AsyncProjectsResourceWithStreamingResponse(self._client.projects) + @cached_property + def api_keys(self) -> api_keys.AsyncAPIKeysResourceWithStreamingResponse: + """Create and manage API keys for organization and project-scoped access.""" + from .resources.api_keys import AsyncAPIKeysResourceWithStreamingResponse + + return AsyncAPIKeysResourceWithStreamingResponse(self._client.api_keys) + @cached_property def credential_providers(self) -> credential_providers.AsyncCredentialProvidersResourceWithStreamingResponse: """Configure external credential providers like 1Password.""" diff --git a/src/kernel/resources/__init__.py b/src/kernel/resources/__init__.py index f078a03b..64b98338 100644 --- a/src/kernel/resources/__init__.py +++ b/src/kernel/resources/__init__.py @@ -24,6 +24,14 @@ ProxiesResourceWithStreamingResponse, AsyncProxiesResourceWithStreamingResponse, ) +from .api_keys import ( + APIKeysResource, + AsyncAPIKeysResource, + APIKeysResourceWithRawResponse, + AsyncAPIKeysResourceWithRawResponse, + APIKeysResourceWithStreamingResponse, + AsyncAPIKeysResourceWithStreamingResponse, +) from .browsers import ( BrowsersResource, AsyncBrowsersResource, @@ -164,6 +172,12 @@ "AsyncProjectsResourceWithRawResponse", "ProjectsResourceWithStreamingResponse", "AsyncProjectsResourceWithStreamingResponse", + "APIKeysResource", + "AsyncAPIKeysResource", + "APIKeysResourceWithRawResponse", + "AsyncAPIKeysResourceWithRawResponse", + "APIKeysResourceWithStreamingResponse", + "AsyncAPIKeysResourceWithStreamingResponse", "CredentialProvidersResource", "AsyncCredentialProvidersResource", "CredentialProvidersResourceWithRawResponse", diff --git a/src/kernel/resources/api_keys.py b/src/kernel/resources/api_keys.py new file mode 100644 index 00000000..aae96794 --- /dev/null +++ b/src/kernel/resources/api_keys.py @@ -0,0 +1,557 @@ +# 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 api_key_list_params, api_key_create_params, api_key_update_params +from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given +from .._utils import path_template, maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..pagination import SyncOffsetPagination, AsyncOffsetPagination +from .._base_client import AsyncPaginator, make_request_options +from ..types.api_key import APIKey +from ..types.created_api_key import CreatedAPIKey + +__all__ = ["APIKeysResource", "AsyncAPIKeysResource"] + + +class APIKeysResource(SyncAPIResource): + """Create and manage API keys for organization and project-scoped access.""" + + @cached_property + def with_raw_response(self) -> APIKeysResourceWithRawResponse: + """ + 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/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return APIKeysResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> APIKeysResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response + """ + return APIKeysResourceWithStreamingResponse(self) + + def create( + self, + *, + name: str, + days_to_expire: Optional[int] | Omit = omit, + project_id: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> CreatedAPIKey: + """ + Create a new API key within the authenticated organization. + + Args: + name: API key name (1-255 characters) + + days_to_expire: Number of days until expiry, up to 3650. Use null for never. + + project_id: Unique project identifier + + 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( + "/org/api_keys", + body=maybe_transform( + { + "name": name, + "days_to_expire": days_to_expire, + "project_id": project_id, + }, + api_key_create_params.APIKeyCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CreatedAPIKey, + ) + + def retrieve( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> APIKey: + """Retrieve an API key by ID for the authenticated organization. + + API keys are + masked. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + path_template("/org/api_keys/{id}", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=APIKey, + ) + + def update( + self, + id: str, + *, + name: 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, + ) -> APIKey: + """ + Update an API key's name. + + Args: + name: New API key name + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._patch( + path_template("/org/api_keys/{id}", id=id), + body=maybe_transform({"name": name}, api_key_update_params.APIKeyUpdateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=APIKey, + ) + + def list( + self, + *, + limit: int | Omit = omit, + offset: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SyncOffsetPagination[APIKey]: + """List API keys for the authenticated organization. + + API keys are masked. + + Args: + limit: Maximum number of results to return + + offset: Number of results to skip + + 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_api_list( + "/org/api_keys", + page=SyncOffsetPagination[APIKey], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "offset": offset, + }, + api_key_list_params.APIKeyListParams, + ), + ), + model=APIKey, + ) + + def delete( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + Delete an API key. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + path_template("/org/api_keys/{id}", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class AsyncAPIKeysResource(AsyncAPIResource): + """Create and manage API keys for organization and project-scoped access.""" + + @cached_property + def with_raw_response(self) -> AsyncAPIKeysResourceWithRawResponse: + """ + 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/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncAPIKeysResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAPIKeysResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response + """ + return AsyncAPIKeysResourceWithStreamingResponse(self) + + async def create( + self, + *, + name: str, + days_to_expire: Optional[int] | Omit = omit, + project_id: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> CreatedAPIKey: + """ + Create a new API key within the authenticated organization. + + Args: + name: API key name (1-255 characters) + + days_to_expire: Number of days until expiry, up to 3650. Use null for never. + + project_id: Unique project identifier + + 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( + "/org/api_keys", + body=await async_maybe_transform( + { + "name": name, + "days_to_expire": days_to_expire, + "project_id": project_id, + }, + api_key_create_params.APIKeyCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CreatedAPIKey, + ) + + async def retrieve( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> APIKey: + """Retrieve an API key by ID for the authenticated organization. + + API keys are + masked. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + path_template("/org/api_keys/{id}", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=APIKey, + ) + + async def update( + self, + id: str, + *, + name: 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, + ) -> APIKey: + """ + Update an API key's name. + + Args: + name: New API key name + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._patch( + path_template("/org/api_keys/{id}", id=id), + body=await async_maybe_transform({"name": name}, api_key_update_params.APIKeyUpdateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=APIKey, + ) + + def list( + self, + *, + limit: int | Omit = omit, + offset: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[APIKey, AsyncOffsetPagination[APIKey]]: + """List API keys for the authenticated organization. + + API keys are masked. + + Args: + limit: Maximum number of results to return + + offset: Number of results to skip + + 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_api_list( + "/org/api_keys", + page=AsyncOffsetPagination[APIKey], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "offset": offset, + }, + api_key_list_params.APIKeyListParams, + ), + ), + model=APIKey, + ) + + async def delete( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + Delete an API key. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + path_template("/org/api_keys/{id}", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class APIKeysResourceWithRawResponse: + def __init__(self, api_keys: APIKeysResource) -> None: + self._api_keys = api_keys + + self.create = to_raw_response_wrapper( + api_keys.create, + ) + self.retrieve = to_raw_response_wrapper( + api_keys.retrieve, + ) + self.update = to_raw_response_wrapper( + api_keys.update, + ) + self.list = to_raw_response_wrapper( + api_keys.list, + ) + self.delete = to_raw_response_wrapper( + api_keys.delete, + ) + + +class AsyncAPIKeysResourceWithRawResponse: + def __init__(self, api_keys: AsyncAPIKeysResource) -> None: + self._api_keys = api_keys + + self.create = async_to_raw_response_wrapper( + api_keys.create, + ) + self.retrieve = async_to_raw_response_wrapper( + api_keys.retrieve, + ) + self.update = async_to_raw_response_wrapper( + api_keys.update, + ) + self.list = async_to_raw_response_wrapper( + api_keys.list, + ) + self.delete = async_to_raw_response_wrapper( + api_keys.delete, + ) + + +class APIKeysResourceWithStreamingResponse: + def __init__(self, api_keys: APIKeysResource) -> None: + self._api_keys = api_keys + + self.create = to_streamed_response_wrapper( + api_keys.create, + ) + self.retrieve = to_streamed_response_wrapper( + api_keys.retrieve, + ) + self.update = to_streamed_response_wrapper( + api_keys.update, + ) + self.list = to_streamed_response_wrapper( + api_keys.list, + ) + self.delete = to_streamed_response_wrapper( + api_keys.delete, + ) + + +class AsyncAPIKeysResourceWithStreamingResponse: + def __init__(self, api_keys: AsyncAPIKeysResource) -> None: + self._api_keys = api_keys + + self.create = async_to_streamed_response_wrapper( + api_keys.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + api_keys.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + api_keys.update, + ) + self.list = async_to_streamed_response_wrapper( + api_keys.list, + ) + self.delete = async_to_streamed_response_wrapper( + api_keys.delete, + ) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 49346e2f..fb6fd589 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -15,15 +15,18 @@ BrowserViewport as BrowserViewport, BrowserExtension as BrowserExtension, ) +from .api_key import APIKey as APIKey from .profile import Profile as Profile from .project import Project as Project from .credential import Credential as Credential from .browser_pool import BrowserPool as BrowserPool from .browser_usage import BrowserUsage as BrowserUsage from .app_list_params import AppListParams as AppListParams +from .created_api_key import CreatedAPIKey as CreatedAPIKey from .browser_pool_ref import BrowserPoolRef as BrowserPoolRef from .app_list_response import AppListResponse as AppListResponse from .proxy_check_params import ProxyCheckParams as ProxyCheckParams +from .api_key_list_params import APIKeyListParams as APIKeyListParams from .browser_curl_params import BrowserCurlParams as BrowserCurlParams from .browser_list_params import BrowserListParams as BrowserListParams from .credential_provider import CredentialProvider as CredentialProvider @@ -32,6 +35,8 @@ from .proxy_create_params import ProxyCreateParams as ProxyCreateParams from .proxy_list_response import ProxyListResponse as ProxyListResponse from .proxy_check_response import ProxyCheckResponse as ProxyCheckResponse +from .api_key_create_params import APIKeyCreateParams as APIKeyCreateParams +from .api_key_update_params import APIKeyUpdateParams as APIKeyUpdateParams from .browser_create_params import BrowserCreateParams as BrowserCreateParams from .browser_curl_response import BrowserCurlResponse as BrowserCurlResponse from .browser_list_response import BrowserListResponse as BrowserListResponse diff --git a/src/kernel/types/api_key.py b/src/kernel/types/api_key.py new file mode 100644 index 00000000..6df577f8 --- /dev/null +++ b/src/kernel/types/api_key.py @@ -0,0 +1,47 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime + +from .._models import BaseModel + +__all__ = ["APIKey", "CreatedBy"] + + +class CreatedBy(BaseModel): + id: str + """Kernel user ID of the creator.""" + + email: str + """Email address of the creator.""" + + name: Optional[str] = None + """Display name of the creator, if available.""" + + +class APIKey(BaseModel): + id: str + """Unique API key identifier""" + + created_at: datetime + """When the API key was created""" + + created_by: CreatedBy + + expires_at: Optional[datetime] = None + """When the API key expires""" + + masked_key: str + """Masked version of the API key""" + + name: str + """API key name""" + + project_id: Optional[str] = None + """Project identifier for project-scoped API keys. Null means org-wide.""" + + project_name: Optional[str] = None + """Project name for project-scoped API keys. + + Null means the key is org-wide or the project name is unavailable. + """ diff --git a/src/kernel/types/api_key_create_params.py b/src/kernel/types/api_key_create_params.py new file mode 100644 index 00000000..7705108f --- /dev/null +++ b/src/kernel/types/api_key_create_params.py @@ -0,0 +1,19 @@ +# 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 Required, TypedDict + +__all__ = ["APIKeyCreateParams"] + + +class APIKeyCreateParams(TypedDict, total=False): + name: Required[str] + """API key name (1-255 characters)""" + + days_to_expire: Optional[int] + """Number of days until expiry, up to 3650. Use null for never.""" + + project_id: Optional[str] + """Unique project identifier""" diff --git a/src/kernel/types/api_key_list_params.py b/src/kernel/types/api_key_list_params.py new file mode 100644 index 00000000..e7d807f2 --- /dev/null +++ b/src/kernel/types/api_key_list_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["APIKeyListParams"] + + +class APIKeyListParams(TypedDict, total=False): + limit: int + """Maximum number of results to return""" + + offset: int + """Number of results to skip""" diff --git a/src/kernel/types/api_key_update_params.py b/src/kernel/types/api_key_update_params.py new file mode 100644 index 00000000..2e917607 --- /dev/null +++ b/src/kernel/types/api_key_update_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["APIKeyUpdateParams"] + + +class APIKeyUpdateParams(TypedDict, total=False): + name: Required[str] + """New API key name""" diff --git a/src/kernel/types/created_api_key.py b/src/kernel/types/created_api_key.py new file mode 100644 index 00000000..674943d5 --- /dev/null +++ b/src/kernel/types/created_api_key.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .api_key import APIKey + +__all__ = ["CreatedAPIKey"] + + +class CreatedAPIKey(APIKey): + """API key returned immediately after creation. Includes the plaintext key once.""" + + key: str + """Plaintext API key. Only returned once when the key is created.""" diff --git a/tests/api_resources/test_api_keys.py b/tests/api_resources/test_api_keys.py new file mode 100644 index 00000000..da854b23 --- /dev/null +++ b/tests/api_resources/test_api_keys.py @@ -0,0 +1,447 @@ +# 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 kernel import Kernel, AsyncKernel +from tests.utils import assert_matches_type +from kernel.types import APIKey, CreatedAPIKey +from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestAPIKeys: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create(self, client: Kernel) -> None: + api_key = client.api_keys.create( + name="staging", + ) + assert_matches_type(CreatedAPIKey, api_key, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create_with_all_params(self, client: Kernel) -> None: + api_key = client.api_keys.create( + name="staging", + days_to_expire=30, + project_id="proj_abc123", + ) + assert_matches_type(CreatedAPIKey, api_key, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_create(self, client: Kernel) -> None: + response = client.api_keys.with_raw_response.create( + name="staging", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + api_key = response.parse() + assert_matches_type(CreatedAPIKey, api_key, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_create(self, client: Kernel) -> None: + with client.api_keys.with_streaming_response.create( + name="staging", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + api_key = response.parse() + assert_matches_type(CreatedAPIKey, api_key, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve(self, client: Kernel) -> None: + api_key = client.api_keys.retrieve( + "id", + ) + assert_matches_type(APIKey, api_key, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: Kernel) -> None: + response = client.api_keys.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + api_key = response.parse() + assert_matches_type(APIKey, api_key, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: Kernel) -> None: + with client.api_keys.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + api_key = response.parse() + assert_matches_type(APIKey, api_key, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_retrieve(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.api_keys.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update(self, client: Kernel) -> None: + api_key = client.api_keys.update( + id="id", + name="new-api-name", + ) + assert_matches_type(APIKey, api_key, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_update(self, client: Kernel) -> None: + response = client.api_keys.with_raw_response.update( + id="id", + name="new-api-name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + api_key = response.parse() + assert_matches_type(APIKey, api_key, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_update(self, client: Kernel) -> None: + with client.api_keys.with_streaming_response.update( + id="id", + name="new-api-name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + api_key = response.parse() + assert_matches_type(APIKey, api_key, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_update(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.api_keys.with_raw_response.update( + id="", + name="new-api-name", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list(self, client: Kernel) -> None: + api_key = client.api_keys.list() + assert_matches_type(SyncOffsetPagination[APIKey], api_key, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_with_all_params(self, client: Kernel) -> None: + api_key = client.api_keys.list( + limit=100, + offset=0, + ) + assert_matches_type(SyncOffsetPagination[APIKey], api_key, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list(self, client: Kernel) -> None: + response = client.api_keys.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + api_key = response.parse() + assert_matches_type(SyncOffsetPagination[APIKey], api_key, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list(self, client: Kernel) -> None: + with client.api_keys.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + api_key = response.parse() + assert_matches_type(SyncOffsetPagination[APIKey], api_key, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_delete(self, client: Kernel) -> None: + api_key = client.api_keys.delete( + "id", + ) + assert api_key is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_delete(self, client: Kernel) -> None: + response = client.api_keys.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + api_key = response.parse() + assert api_key is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_delete(self, client: Kernel) -> None: + with client.api_keys.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + api_key = response.parse() + assert api_key is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_delete(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.api_keys.with_raw_response.delete( + "", + ) + + +class TestAsyncAPIKeys: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create(self, async_client: AsyncKernel) -> None: + api_key = await async_client.api_keys.create( + name="staging", + ) + assert_matches_type(CreatedAPIKey, api_key, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: + api_key = await async_client.api_keys.create( + name="staging", + days_to_expire=30, + project_id="proj_abc123", + ) + assert_matches_type(CreatedAPIKey, api_key, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_create(self, async_client: AsyncKernel) -> None: + response = await async_client.api_keys.with_raw_response.create( + name="staging", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + api_key = await response.parse() + assert_matches_type(CreatedAPIKey, api_key, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: + async with async_client.api_keys.with_streaming_response.create( + name="staging", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + api_key = await response.parse() + assert_matches_type(CreatedAPIKey, api_key, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve(self, async_client: AsyncKernel) -> None: + api_key = await async_client.api_keys.retrieve( + "id", + ) + assert_matches_type(APIKey, api_key, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: + response = await async_client.api_keys.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + api_key = await response.parse() + assert_matches_type(APIKey, api_key, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: + async with async_client.api_keys.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + api_key = await response.parse() + assert_matches_type(APIKey, api_key, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.api_keys.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update(self, async_client: AsyncKernel) -> None: + api_key = await async_client.api_keys.update( + id="id", + name="new-api-name", + ) + assert_matches_type(APIKey, api_key, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_update(self, async_client: AsyncKernel) -> None: + response = await async_client.api_keys.with_raw_response.update( + id="id", + name="new-api-name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + api_key = await response.parse() + assert_matches_type(APIKey, api_key, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_update(self, async_client: AsyncKernel) -> None: + async with async_client.api_keys.with_streaming_response.update( + id="id", + name="new-api-name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + api_key = await response.parse() + assert_matches_type(APIKey, api_key, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_update(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.api_keys.with_raw_response.update( + id="", + name="new-api-name", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncKernel) -> None: + api_key = await async_client.api_keys.list() + assert_matches_type(AsyncOffsetPagination[APIKey], api_key, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: + api_key = await async_client.api_keys.list( + limit=100, + offset=0, + ) + assert_matches_type(AsyncOffsetPagination[APIKey], api_key, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncKernel) -> None: + response = await async_client.api_keys.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + api_key = await response.parse() + assert_matches_type(AsyncOffsetPagination[APIKey], api_key, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: + async with async_client.api_keys.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + api_key = await response.parse() + assert_matches_type(AsyncOffsetPagination[APIKey], api_key, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_delete(self, async_client: AsyncKernel) -> None: + api_key = await async_client.api_keys.delete( + "id", + ) + assert api_key is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: + response = await async_client.api_keys.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + api_key = await response.parse() + assert api_key is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None: + async with async_client.api_keys.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + api_key = await response.parse() + assert api_key is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_delete(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.api_keys.with_raw_response.delete( + "", + ) From 71ca62b9654744c863e7f75e0294b8be994561e1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 27 May 2026 17:08:51 +0000 Subject: [PATCH 409/448] feat: Fix API key request model SDK metadata --- .stats.yml | 4 ++-- api.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index e86e3342..3cfc09d4 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 117 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-54d9016df9730e65881df9f694df4f33620b0c032f177849d36ccc426cd42fa8.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-3b34d85c005a4058ac1faaea092615af577d12cee6e420f102de57339251672d.yml openapi_spec_hash: fad386b8e8712e6639ed9689e9dfc070 -config_hash: 58396d6c1fafaf72b26596b9117edf48 +config_hash: 0f222358f24700d1811c5d27078a3849 diff --git a/api.md b/api.md index 1dc6fece..c9e1b290 100644 --- a/api.md +++ b/api.md @@ -419,7 +419,7 @@ Methods: Types: ```python -from kernel.types import APIKey, CreateAPIKeyRequest, CreatedAPIKey, UpdateAPIKeyRequest +from kernel.types import APIKey, CreatedAPIKey ``` Methods: From 51d416ef7e609ae4cd2dfa8708e626c0e0bd5f92 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 27 May 2026 17:54:48 +0000 Subject: [PATCH 410/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 2afb750c..2fbefb94 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.57.0" + ".": "0.58.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 6956b281..3ff0ba88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.57.0" +version = "0.58.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index fb252c57..9422de86 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.57.0" # x-release-please-version +__version__ = "0.58.0" # x-release-please-version From 9495bcd3f84543914460e5dfc1ca167a730be46c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 27 May 2026 20:48:05 +0000 Subject: [PATCH 411/448] feat: Support Byteful mobile proxies --- .stats.yml | 4 +- src/kernel/types/proxy_check_response.py | 77 +-------------------- src/kernel/types/proxy_create_params.py | 75 +------------------- src/kernel/types/proxy_create_response.py | 77 +-------------------- src/kernel/types/proxy_list_response.py | 77 +-------------------- src/kernel/types/proxy_retrieve_response.py | 77 +-------------------- 6 files changed, 12 insertions(+), 375 deletions(-) diff --git a/.stats.yml b/.stats.yml index 3cfc09d4..834590a5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 117 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-3b34d85c005a4058ac1faaea092615af577d12cee6e420f102de57339251672d.yml -openapi_spec_hash: fad386b8e8712e6639ed9689e9dfc070 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-a32ac633a8f67f3844b6ccb7b97687aec2cf2e2c611df4157c223dfac16db806.yml +openapi_spec_hash: f8c9aabe60372f28ad9cceed42009274 config_hash: 0f222358f24700d1811c5d27078a3849 diff --git a/src/kernel/types/proxy_check_response.py b/src/kernel/types/proxy_check_response.py index c26d665d..f3cdab80 100644 --- a/src/kernel/types/proxy_check_response.py +++ b/src/kernel/types/proxy_check_response.py @@ -59,87 +59,14 @@ class ConfigResidentialProxyConfig(BaseModel): class ConfigMobileProxyConfig(BaseModel): """Configuration for mobile proxies.""" - asn: Optional[str] = None - """Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html""" - - carrier: Optional[ - Literal[ - "a1", - "aircel", - "airtel", - "att", - "celcom", - "chinamobile", - "claro", - "comcast", - "cox", - "digi", - "dt", - "docomo", - "dtac", - "etisalat", - "idea", - "kyivstar", - "meo", - "megafon", - "mtn", - "mtnza", - "mts", - "optus", - "orange", - "qwest", - "reliance_jio", - "robi", - "sprint", - "telefonica", - "telstra", - "tmobile", - "tigo", - "tim", - "verizon", - "vimpelcom", - "vodacomza", - "vodafone", - "vivo", - "zain", - "vivabo", - "telenormyanmar", - "kcelljsc", - "swisscom", - "singtel", - "asiacell", - "windit", - "cellc", - "ooredoo", - "drei", - "umobile", - "cableone", - "proximus", - "tele2", - "mobitel", - "o2", - "bouygues", - "free", - "sfr", - "digicel", - ] - ] = None - """Mobile carrier.""" - city: Optional[str] = None - """City name (no spaces, e.g. - - `sanfrancisco`). If provided, `country` must also be provided. - """ + """Provider city alias. Mobile carrier routing can make observed geo vary.""" country: Optional[str] = None """ISO 3166 country code""" state: Optional[str] = None - """Two-letter state code.""" - - zip: Optional[str] = None - """US ZIP code.""" + """US-only state code. Mobile carrier routing can make observed geo vary.""" class ConfigCustomProxyConfig(BaseModel): diff --git a/src/kernel/types/proxy_create_params.py b/src/kernel/types/proxy_create_params.py index 175b95ff..331bd776 100644 --- a/src/kernel/types/proxy_create_params.py +++ b/src/kernel/types/proxy_create_params.py @@ -81,85 +81,14 @@ class ConfigResidentialProxyConfig(TypedDict, total=False): class ConfigMobileProxyConfig(TypedDict, total=False): """Configuration for mobile proxies.""" - asn: str - """Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html""" - - carrier: Literal[ - "a1", - "aircel", - "airtel", - "att", - "celcom", - "chinamobile", - "claro", - "comcast", - "cox", - "digi", - "dt", - "docomo", - "dtac", - "etisalat", - "idea", - "kyivstar", - "meo", - "megafon", - "mtn", - "mtnza", - "mts", - "optus", - "orange", - "qwest", - "reliance_jio", - "robi", - "sprint", - "telefonica", - "telstra", - "tmobile", - "tigo", - "tim", - "verizon", - "vimpelcom", - "vodacomza", - "vodafone", - "vivo", - "zain", - "vivabo", - "telenormyanmar", - "kcelljsc", - "swisscom", - "singtel", - "asiacell", - "windit", - "cellc", - "ooredoo", - "drei", - "umobile", - "cableone", - "proximus", - "tele2", - "mobitel", - "o2", - "bouygues", - "free", - "sfr", - "digicel", - ] - """Mobile carrier.""" - city: str - """City name (no spaces, e.g. - - `sanfrancisco`). If provided, `country` must also be provided. - """ + """Provider city alias. Mobile carrier routing can make observed geo vary.""" country: str """ISO 3166 country code""" state: str - """Two-letter state code.""" - - zip: str - """US ZIP code.""" + """US-only state code. Mobile carrier routing can make observed geo vary.""" class ConfigCreateCustomProxyConfig(TypedDict, total=False): diff --git a/src/kernel/types/proxy_create_response.py b/src/kernel/types/proxy_create_response.py index d317662f..d71656c8 100644 --- a/src/kernel/types/proxy_create_response.py +++ b/src/kernel/types/proxy_create_response.py @@ -59,87 +59,14 @@ class ConfigResidentialProxyConfig(BaseModel): class ConfigMobileProxyConfig(BaseModel): """Configuration for mobile proxies.""" - asn: Optional[str] = None - """Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html""" - - carrier: Optional[ - Literal[ - "a1", - "aircel", - "airtel", - "att", - "celcom", - "chinamobile", - "claro", - "comcast", - "cox", - "digi", - "dt", - "docomo", - "dtac", - "etisalat", - "idea", - "kyivstar", - "meo", - "megafon", - "mtn", - "mtnza", - "mts", - "optus", - "orange", - "qwest", - "reliance_jio", - "robi", - "sprint", - "telefonica", - "telstra", - "tmobile", - "tigo", - "tim", - "verizon", - "vimpelcom", - "vodacomza", - "vodafone", - "vivo", - "zain", - "vivabo", - "telenormyanmar", - "kcelljsc", - "swisscom", - "singtel", - "asiacell", - "windit", - "cellc", - "ooredoo", - "drei", - "umobile", - "cableone", - "proximus", - "tele2", - "mobitel", - "o2", - "bouygues", - "free", - "sfr", - "digicel", - ] - ] = None - """Mobile carrier.""" - city: Optional[str] = None - """City name (no spaces, e.g. - - `sanfrancisco`). If provided, `country` must also be provided. - """ + """Provider city alias. Mobile carrier routing can make observed geo vary.""" country: Optional[str] = None """ISO 3166 country code""" state: Optional[str] = None - """Two-letter state code.""" - - zip: Optional[str] = None - """US ZIP code.""" + """US-only state code. Mobile carrier routing can make observed geo vary.""" class ConfigCustomProxyConfig(BaseModel): diff --git a/src/kernel/types/proxy_list_response.py b/src/kernel/types/proxy_list_response.py index bbbe17c7..d1ddc079 100644 --- a/src/kernel/types/proxy_list_response.py +++ b/src/kernel/types/proxy_list_response.py @@ -60,87 +60,14 @@ class ProxyListResponseItemConfigResidentialProxyConfig(BaseModel): class ProxyListResponseItemConfigMobileProxyConfig(BaseModel): """Configuration for mobile proxies.""" - asn: Optional[str] = None - """Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html""" - - carrier: Optional[ - Literal[ - "a1", - "aircel", - "airtel", - "att", - "celcom", - "chinamobile", - "claro", - "comcast", - "cox", - "digi", - "dt", - "docomo", - "dtac", - "etisalat", - "idea", - "kyivstar", - "meo", - "megafon", - "mtn", - "mtnza", - "mts", - "optus", - "orange", - "qwest", - "reliance_jio", - "robi", - "sprint", - "telefonica", - "telstra", - "tmobile", - "tigo", - "tim", - "verizon", - "vimpelcom", - "vodacomza", - "vodafone", - "vivo", - "zain", - "vivabo", - "telenormyanmar", - "kcelljsc", - "swisscom", - "singtel", - "asiacell", - "windit", - "cellc", - "ooredoo", - "drei", - "umobile", - "cableone", - "proximus", - "tele2", - "mobitel", - "o2", - "bouygues", - "free", - "sfr", - "digicel", - ] - ] = None - """Mobile carrier.""" - city: Optional[str] = None - """City name (no spaces, e.g. - - `sanfrancisco`). If provided, `country` must also be provided. - """ + """Provider city alias. Mobile carrier routing can make observed geo vary.""" country: Optional[str] = None """ISO 3166 country code""" state: Optional[str] = None - """Two-letter state code.""" - - zip: Optional[str] = None - """US ZIP code.""" + """US-only state code. Mobile carrier routing can make observed geo vary.""" class ProxyListResponseItemConfigCustomProxyConfig(BaseModel): diff --git a/src/kernel/types/proxy_retrieve_response.py b/src/kernel/types/proxy_retrieve_response.py index 6b0b1bbe..77d113e5 100644 --- a/src/kernel/types/proxy_retrieve_response.py +++ b/src/kernel/types/proxy_retrieve_response.py @@ -59,87 +59,14 @@ class ConfigResidentialProxyConfig(BaseModel): class ConfigMobileProxyConfig(BaseModel): """Configuration for mobile proxies.""" - asn: Optional[str] = None - """Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html""" - - carrier: Optional[ - Literal[ - "a1", - "aircel", - "airtel", - "att", - "celcom", - "chinamobile", - "claro", - "comcast", - "cox", - "digi", - "dt", - "docomo", - "dtac", - "etisalat", - "idea", - "kyivstar", - "meo", - "megafon", - "mtn", - "mtnza", - "mts", - "optus", - "orange", - "qwest", - "reliance_jio", - "robi", - "sprint", - "telefonica", - "telstra", - "tmobile", - "tigo", - "tim", - "verizon", - "vimpelcom", - "vodacomza", - "vodafone", - "vivo", - "zain", - "vivabo", - "telenormyanmar", - "kcelljsc", - "swisscom", - "singtel", - "asiacell", - "windit", - "cellc", - "ooredoo", - "drei", - "umobile", - "cableone", - "proximus", - "tele2", - "mobitel", - "o2", - "bouygues", - "free", - "sfr", - "digicel", - ] - ] = None - """Mobile carrier.""" - city: Optional[str] = None - """City name (no spaces, e.g. - - `sanfrancisco`). If provided, `country` must also be provided. - """ + """Provider city alias. Mobile carrier routing can make observed geo vary.""" country: Optional[str] = None """ISO 3166 country code""" state: Optional[str] = None - """Two-letter state code.""" - - zip: Optional[str] = None - """US ZIP code.""" + """US-only state code. Mobile carrier routing can make observed geo vary.""" class ConfigCustomProxyConfig(BaseModel): From 0a8549b319a42bf913659f53467388bda0e3bf36 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 29 May 2026 16:18:51 +0000 Subject: [PATCH 412/448] codegen metadata --- .stats.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 834590a5..190a6ab6 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 117 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-a32ac633a8f67f3844b6ccb7b97687aec2cf2e2c611df4157c223dfac16db806.yml -openapi_spec_hash: f8c9aabe60372f28ad9cceed42009274 -config_hash: 0f222358f24700d1811c5d27078a3849 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-04da2c2ed5f83c54f59f6c148abcf013cc37282fde2b3b5b263dffab927d5ba2.yml +openapi_spec_hash: 9b05d6877797e55051a83222fa7652d0 +config_hash: e0741f8035aea13f71e54e0fdb88eaa4 From 140bfa72aca05b55eb86ce7acc9062ec8abf0855 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 29 May 2026 17:40:24 +0000 Subject: [PATCH 413/448] feat: api: surface category field on browser telemetry events --- .stats.yml | 4 ++-- src/kernel/types/browsers/browser_console_error_event.py | 2 ++ src/kernel/types/browsers/browser_console_log_event.py | 2 ++ src/kernel/types/browsers/browser_interaction_click_event.py | 2 ++ src/kernel/types/browsers/browser_interaction_key_event.py | 2 ++ .../browsers/browser_interaction_scroll_settled_event.py | 2 ++ .../types/browsers/browser_monitor_disconnected_event.py | 2 ++ .../types/browsers/browser_monitor_init_failed_event.py | 2 ++ .../types/browsers/browser_monitor_reconnect_failed_event.py | 2 ++ .../types/browsers/browser_monitor_reconnected_event.py | 2 ++ src/kernel/types/browsers/browser_monitor_screenshot_event.py | 2 ++ src/kernel/types/browsers/browser_network_idle_event.py | 2 ++ .../types/browsers/browser_network_loading_failed_event.py | 2 ++ src/kernel/types/browsers/browser_network_request_event.py | 2 ++ src/kernel/types/browsers/browser_network_response_event.py | 2 ++ .../types/browsers/browser_page_dom_content_loaded_event.py | 2 ++ .../types/browsers/browser_page_layout_settled_event.py | 2 ++ src/kernel/types/browsers/browser_page_layout_shift_event.py | 2 ++ src/kernel/types/browsers/browser_page_lcp_event.py | 2 ++ src/kernel/types/browsers/browser_page_load_event.py | 2 ++ src/kernel/types/browsers/browser_page_navigation_event.py | 2 ++ .../types/browsers/browser_page_navigation_settled_event.py | 2 ++ src/kernel/types/browsers/browser_page_tab_opened_event.py | 2 ++ 23 files changed, 46 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 190a6ab6..f41061ca 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 117 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-04da2c2ed5f83c54f59f6c148abcf013cc37282fde2b3b5b263dffab927d5ba2.yml -openapi_spec_hash: 9b05d6877797e55051a83222fa7652d0 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-4bada2a5bdbde93018e5a1b1e80e134acbc5509cfdea94db6e4c5b799eba7b82.yml +openapi_spec_hash: e0d541d480f5663b1e6bd3bb19a0fe61 config_hash: e0741f8035aea13f71e54e0fdb88eaa4 diff --git a/src/kernel/types/browsers/browser_console_error_event.py b/src/kernel/types/browsers/browser_console_error_event.py index a152f944..72a93ede 100644 --- a/src/kernel/types/browsers/browser_console_error_event.py +++ b/src/kernel/types/browsers/browser_console_error_event.py @@ -65,6 +65,8 @@ class BrowserConsoleErrorEvent(BaseModel): Emitted from two distinct CDP sources with different data shapes. Runtime.consoleAPICalled (console.error calls) produces level, text, args, and stack_trace. Runtime.exceptionThrown (uncaught exceptions) produces text, line, column, source_url, and stack_trace. Fields not applicable to the source are absent. """ + category: Literal["console"] + source: BrowserEventSource """Provenance metadata identifying which producer emitted the event.""" diff --git a/src/kernel/types/browsers/browser_console_log_event.py b/src/kernel/types/browsers/browser_console_log_event.py index 5994c0be..338f6c85 100644 --- a/src/kernel/types/browsers/browser_console_log_event.py +++ b/src/kernel/types/browsers/browser_console_log_event.py @@ -42,6 +42,8 @@ class Data(BrowserEventContext): class BrowserConsoleLogEvent(BaseModel): """A browser console log event (console.log, console.info, console.warn, etc.).""" + category: Literal["console"] + source: BrowserEventSource """Provenance metadata identifying which producer emitted the event.""" diff --git a/src/kernel/types/browsers/browser_interaction_click_event.py b/src/kernel/types/browsers/browser_interaction_click_event.py index efc76b34..7c4f6119 100644 --- a/src/kernel/types/browsers/browser_interaction_click_event.py +++ b/src/kernel/types/browsers/browser_interaction_click_event.py @@ -35,6 +35,8 @@ class Data(BrowserEventContext): class BrowserInteractionClickEvent(BaseModel): """A browser user click event captured via injected page script.""" + category: Literal["interaction"] + source: BrowserEventSource """Provenance metadata identifying which producer emitted the event.""" diff --git a/src/kernel/types/browsers/browser_interaction_key_event.py b/src/kernel/types/browsers/browser_interaction_key_event.py index e8860330..93f2c43e 100644 --- a/src/kernel/types/browsers/browser_interaction_key_event.py +++ b/src/kernel/types/browsers/browser_interaction_key_event.py @@ -29,6 +29,8 @@ class Data(BrowserEventContext): class BrowserInteractionKeyEvent(BaseModel): """A browser keyboard event captured via injected page script.""" + category: Literal["interaction"] + source: BrowserEventSource """Provenance metadata identifying which producer emitted the event.""" diff --git a/src/kernel/types/browsers/browser_interaction_scroll_settled_event.py b/src/kernel/types/browsers/browser_interaction_scroll_settled_event.py index 16b7536b..2c7ee24c 100644 --- a/src/kernel/types/browsers/browser_interaction_scroll_settled_event.py +++ b/src/kernel/types/browsers/browser_interaction_scroll_settled_event.py @@ -37,6 +37,8 @@ class BrowserInteractionScrollSettledEvent(BaseModel): A browser scroll settled event emitted after scroll position stops changing, captured via injected page script. """ + category: Literal["interaction"] + source: BrowserEventSource """Provenance metadata identifying which producer emitted the event.""" diff --git a/src/kernel/types/browsers/browser_monitor_disconnected_event.py b/src/kernel/types/browsers/browser_monitor_disconnected_event.py index b329ab13..ac37ec6a 100644 --- a/src/kernel/types/browsers/browser_monitor_disconnected_event.py +++ b/src/kernel/types/browsers/browser_monitor_disconnected_event.py @@ -20,6 +20,8 @@ class BrowserMonitorDisconnectedEvent(BaseModel): Telemetry events may be dropped until monitor_reconnected arrives. Treat any in-progress computed state (network_idle, page_layout_settled) as unreliable until then. """ + category: Literal["system"] + source: BrowserEventSource """Provenance metadata identifying which producer emitted the event.""" diff --git a/src/kernel/types/browsers/browser_monitor_init_failed_event.py b/src/kernel/types/browsers/browser_monitor_init_failed_event.py index a745fb8d..dba0e5da 100644 --- a/src/kernel/types/browsers/browser_monitor_init_failed_event.py +++ b/src/kernel/types/browsers/browser_monitor_init_failed_event.py @@ -17,6 +17,8 @@ class Data(BaseModel): class BrowserMonitorInitFailedEvent(BaseModel): """The CDP session could not be initialized.""" + category: Literal["system"] + source: BrowserEventSource """Provenance metadata identifying which producer emitted the event.""" diff --git a/src/kernel/types/browsers/browser_monitor_reconnect_failed_event.py b/src/kernel/types/browsers/browser_monitor_reconnect_failed_event.py index 79308d2e..57eec69b 100644 --- a/src/kernel/types/browsers/browser_monitor_reconnect_failed_event.py +++ b/src/kernel/types/browsers/browser_monitor_reconnect_failed_event.py @@ -23,6 +23,8 @@ class BrowserMonitorReconnectFailedEvent(BaseModel): The CDP connection to Chrome could not be re-established after exhausting all reconnection attempts. No further telemetry events will arrive on this session. """ + category: Literal["system"] + source: BrowserEventSource """Provenance metadata identifying which producer emitted the event.""" diff --git a/src/kernel/types/browsers/browser_monitor_reconnected_event.py b/src/kernel/types/browsers/browser_monitor_reconnected_event.py index a49ad351..39363367 100644 --- a/src/kernel/types/browsers/browser_monitor_reconnected_event.py +++ b/src/kernel/types/browsers/browser_monitor_reconnected_event.py @@ -19,6 +19,8 @@ class BrowserMonitorReconnectedEvent(BaseModel): The CDP connection to Chrome was successfully re-established after a disconnection. Events emitted during the gap are lost. Computed state is reset, so navigation and network tracking restart fresh from this point. """ + category: Literal["system"] + source: BrowserEventSource """Provenance metadata identifying which producer emitted the event.""" diff --git a/src/kernel/types/browsers/browser_monitor_screenshot_event.py b/src/kernel/types/browsers/browser_monitor_screenshot_event.py index 9b446fa9..bd2dc65b 100644 --- a/src/kernel/types/browsers/browser_monitor_screenshot_event.py +++ b/src/kernel/types/browsers/browser_monitor_screenshot_event.py @@ -17,6 +17,8 @@ class Data(BaseModel): class BrowserMonitorScreenshotEvent(BaseModel): """A periodic screenshot of the browser viewport.""" + category: Literal["system"] + source: BrowserEventSource """Provenance metadata identifying which producer emitted the event.""" diff --git a/src/kernel/types/browsers/browser_network_idle_event.py b/src/kernel/types/browsers/browser_network_idle_event.py index 4b4fae6b..b1877115 100644 --- a/src/kernel/types/browsers/browser_network_idle_event.py +++ b/src/kernel/types/browsers/browser_network_idle_event.py @@ -15,6 +15,8 @@ class BrowserNetworkIdleEvent(BaseModel): A browser network idle event emitted after a 500ms quiet period with no in-flight HTTP requests. """ + category: Literal["network"] + source: BrowserEventSource """Provenance metadata identifying which producer emitted the event.""" diff --git a/src/kernel/types/browsers/browser_network_loading_failed_event.py b/src/kernel/types/browsers/browser_network_loading_failed_event.py index bf566a6b..ac6b7961 100644 --- a/src/kernel/types/browsers/browser_network_loading_failed_event.py +++ b/src/kernel/types/browsers/browser_network_loading_failed_event.py @@ -39,6 +39,8 @@ class BrowserNetworkLoadingFailedEvent(BaseModel): If the request was already in flight when CDP attached (no prior network_request was emitted for it), url, frame_id, loader_id, and resource_type are absent; BrowserEventContext is partially populated in that case. """ + category: Literal["network"] + source: BrowserEventSource """Provenance metadata identifying which producer emitted the event.""" diff --git a/src/kernel/types/browsers/browser_network_request_event.py b/src/kernel/types/browsers/browser_network_request_event.py index e1bd9428..efea950b 100644 --- a/src/kernel/types/browsers/browser_network_request_event.py +++ b/src/kernel/types/browsers/browser_network_request_event.py @@ -55,6 +55,8 @@ class Data(BrowserEventContext): class BrowserNetworkRequestEvent(BaseModel): """A browser network request sent event.""" + category: Literal["network"] + source: BrowserEventSource """Provenance metadata identifying which producer emitted the event.""" diff --git a/src/kernel/types/browsers/browser_network_response_event.py b/src/kernel/types/browsers/browser_network_response_event.py index 8a71d24a..b39fefb8 100644 --- a/src/kernel/types/browsers/browser_network_response_event.py +++ b/src/kernel/types/browsers/browser_network_response_event.py @@ -52,6 +52,8 @@ class BrowserNetworkResponseEvent(BaseModel): Fired after the response body is fully received, not when headers arrive. """ + category: Literal["network"] + source: BrowserEventSource """Provenance metadata identifying which producer emitted the event.""" diff --git a/src/kernel/types/browsers/browser_page_dom_content_loaded_event.py b/src/kernel/types/browsers/browser_page_dom_content_loaded_event.py index e917ca32..99393cce 100644 --- a/src/kernel/types/browsers/browser_page_dom_content_loaded_event.py +++ b/src/kernel/types/browsers/browser_page_dom_content_loaded_event.py @@ -26,6 +26,8 @@ class Data(BrowserEventContext): class BrowserPageDomContentLoadedEvent(BaseModel): """A browser DOMContentLoaded event (CDP Page.domContentEventFired).""" + category: Literal["page"] + source: BrowserEventSource """Provenance metadata identifying which producer emitted the event.""" diff --git a/src/kernel/types/browsers/browser_page_layout_settled_event.py b/src/kernel/types/browsers/browser_page_layout_settled_event.py index 289b53a5..f0558f12 100644 --- a/src/kernel/types/browsers/browser_page_layout_settled_event.py +++ b/src/kernel/types/browsers/browser_page_layout_settled_event.py @@ -15,6 +15,8 @@ class BrowserPageLayoutSettledEvent(BaseModel): A browser layout settled event emitted 1 second after page load with no intervening layout shifts, indicating visual stability. Each layout shift resets the 1-second timer. """ + category: Literal["page"] + source: BrowserEventSource """Provenance metadata identifying which producer emitted the event.""" diff --git a/src/kernel/types/browsers/browser_page_layout_shift_event.py b/src/kernel/types/browsers/browser_page_layout_shift_event.py index 336bfce2..fb578ac8 100644 --- a/src/kernel/types/browsers/browser_page_layout_shift_event.py +++ b/src/kernel/types/browsers/browser_page_layout_shift_event.py @@ -50,6 +50,8 @@ class BrowserPageLayoutShiftEvent(BaseModel): A browser cumulative layout shift (CLS) event from the Performance Timeline API. """ + category: Literal["page"] + source: BrowserEventSource """Provenance metadata identifying which producer emitted the event.""" diff --git a/src/kernel/types/browsers/browser_page_lcp_event.py b/src/kernel/types/browsers/browser_page_lcp_event.py index 13b9c270..0bcbddaa 100644 --- a/src/kernel/types/browsers/browser_page_lcp_event.py +++ b/src/kernel/types/browsers/browser_page_lcp_event.py @@ -56,6 +56,8 @@ class BrowserPageLcpEvent(BaseModel): A browser Largest Contentful Paint (LCP) event from the Performance Timeline API. """ + category: Literal["page"] + source: BrowserEventSource """Provenance metadata identifying which producer emitted the event.""" diff --git a/src/kernel/types/browsers/browser_page_load_event.py b/src/kernel/types/browsers/browser_page_load_event.py index b4d85c8f..f27658f1 100644 --- a/src/kernel/types/browsers/browser_page_load_event.py +++ b/src/kernel/types/browsers/browser_page_load_event.py @@ -26,6 +26,8 @@ class Data(BrowserEventContext): class BrowserPageLoadEvent(BaseModel): """A browser page load event (CDP Page.loadEventFired).""" + category: Literal["page"] + source: BrowserEventSource """Provenance metadata identifying which producer emitted the event.""" diff --git a/src/kernel/types/browsers/browser_page_navigation_event.py b/src/kernel/types/browsers/browser_page_navigation_event.py index f857398c..5835d6d4 100644 --- a/src/kernel/types/browsers/browser_page_navigation_event.py +++ b/src/kernel/types/browsers/browser_page_navigation_event.py @@ -41,6 +41,8 @@ class BrowserPageNavigationEvent(BaseModel): Carries nav context fields inline but not nav_seq, as this event resets the navigation epoch. """ + category: Literal["page"] + source: BrowserEventSource """Provenance metadata identifying which producer emitted the event.""" diff --git a/src/kernel/types/browsers/browser_page_navigation_settled_event.py b/src/kernel/types/browsers/browser_page_navigation_settled_event.py index b5227ff4..6e5d31c2 100644 --- a/src/kernel/types/browsers/browser_page_navigation_settled_event.py +++ b/src/kernel/types/browsers/browser_page_navigation_settled_event.py @@ -15,6 +15,8 @@ class BrowserPageNavigationSettledEvent(BaseModel): Emitted when page_dom_content_loaded and page_layout_settled have both fired for the same navigation, indicating the page is loaded and visually stable. Independent of network_idle; a single pending request does not block it. """ + category: Literal["page"] + source: BrowserEventSource """Provenance metadata identifying which producer emitted the event.""" diff --git a/src/kernel/types/browsers/browser_page_tab_opened_event.py b/src/kernel/types/browsers/browser_page_tab_opened_event.py index 9831aee1..144be476 100644 --- a/src/kernel/types/browsers/browser_page_tab_opened_event.py +++ b/src/kernel/types/browsers/browser_page_tab_opened_event.py @@ -31,6 +31,8 @@ class BrowserPageTabOpenedEvent(BaseModel): A new browser tab or target was opened (CDP Target.attachedToTarget for page targets). Fires before a CDP session is attached to the new target, so session_id, frame_id, loader_id, and nav_seq are absent; this event does not compose BrowserEventContext. Consumers reading context fields generically should treat it as a special case. """ + category: Literal["page"] + source: BrowserEventSource """Provenance metadata identifying which producer emitted the event.""" From ace0a711d4aa6cb5c2d1fc7b80976f0ffbcd8e4e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 14:20:33 +0000 Subject: [PATCH 414/448] fix(api): move batch + get_mouse_position into Browser Computer Controls tag --- .stats.yml | 4 ++-- src/kernel/resources/browsers/browsers.py | 6 ++++++ src/kernel/resources/browsers/computer.py | 4 ++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index f41061ca..8ba534ce 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 117 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-4bada2a5bdbde93018e5a1b1e80e134acbc5509cfdea94db6e4c5b799eba7b82.yml -openapi_spec_hash: e0d541d480f5663b1e6bd3bb19a0fe61 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-7548abe9fea7553e12a128b31f7dc7fc33f2558a9f20d2eb33464db5f61ab3ea.yml +openapi_spec_hash: 4b701cc4b33a2c944a5a9b63e9f364a2 config_hash: e0741f8035aea13f71e54e0fdb88eaa4 diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 3644c019..f2c388dc 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -126,6 +126,7 @@ def logs(self) -> LogsResource: @cached_property def computer(self) -> ComputerResource: + """Control mouse, keyboard, and screen on the browser instance.""" return ComputerResource(self._client) @cached_property @@ -615,6 +616,7 @@ def logs(self) -> AsyncLogsResource: @cached_property def computer(self) -> AsyncComputerResource: + """Control mouse, keyboard, and screen on the browser instance.""" return AsyncComputerResource(self._client) @cached_property @@ -1127,6 +1129,7 @@ def logs(self) -> LogsResourceWithRawResponse: @cached_property def computer(self) -> ComputerResourceWithRawResponse: + """Control mouse, keyboard, and screen on the browser instance.""" return ComputerResourceWithRawResponse(self._browsers.computer) @cached_property @@ -1188,6 +1191,7 @@ def logs(self) -> AsyncLogsResourceWithRawResponse: @cached_property def computer(self) -> AsyncComputerResourceWithRawResponse: + """Control mouse, keyboard, and screen on the browser instance.""" return AsyncComputerResourceWithRawResponse(self._browsers.computer) @cached_property @@ -1249,6 +1253,7 @@ def logs(self) -> LogsResourceWithStreamingResponse: @cached_property def computer(self) -> ComputerResourceWithStreamingResponse: + """Control mouse, keyboard, and screen on the browser instance.""" return ComputerResourceWithStreamingResponse(self._browsers.computer) @cached_property @@ -1310,6 +1315,7 @@ def logs(self) -> AsyncLogsResourceWithStreamingResponse: @cached_property def computer(self) -> AsyncComputerResourceWithStreamingResponse: + """Control mouse, keyboard, and screen on the browser instance.""" return AsyncComputerResourceWithStreamingResponse(self._browsers.computer) @cached_property diff --git a/src/kernel/resources/browsers/computer.py b/src/kernel/resources/browsers/computer.py index 54b638e5..d2b57304 100644 --- a/src/kernel/resources/browsers/computer.py +++ b/src/kernel/resources/browsers/computer.py @@ -46,6 +46,8 @@ class ComputerResource(SyncAPIResource): + """Control mouse, keyboard, and screen on the browser instance.""" + @cached_property def with_raw_response(self) -> ComputerResourceWithRawResponse: """ @@ -649,6 +651,8 @@ def write_clipboard( class AsyncComputerResource(AsyncAPIResource): + """Control mouse, keyboard, and screen on the browser instance.""" + @cached_property def with_raw_response(self) -> AsyncComputerResourceWithRawResponse: """ From f876fb56748b01df6f16905f65a2f318814ad3dd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 15:53:42 +0000 Subject: [PATCH 415/448] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 8ba534ce..e31b9566 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 117 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-7548abe9fea7553e12a128b31f7dc7fc33f2558a9f20d2eb33464db5f61ab3ea.yml -openapi_spec_hash: 4b701cc4b33a2c944a5a9b63e9f364a2 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-501a71a04e6a68427e378df10c7327025e3cb177f4550459b5a6eb752a177a25.yml +openapi_spec_hash: d38661081dd50fded2208424bb19157b config_hash: e0741f8035aea13f71e54e0fdb88eaa4 From c0229a0be2bf952586ccfbaf5f1dc3b4528ad28f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:22:06 +0000 Subject: [PATCH 416/448] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index e31b9566..8fee23fa 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 117 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-501a71a04e6a68427e378df10c7327025e3cb177f4550459b5a6eb752a177a25.yml -openapi_spec_hash: d38661081dd50fded2208424bb19157b +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-7988523e72ff96a93d2913e63ba27c5bb94af9893b94fceeeda64e0b43fb0663.yml +openapi_spec_hash: ce2ae9f9137d4c2bf947d446d00ebec8 config_hash: e0741f8035aea13f71e54e0fdb88eaa4 From 4f17cdec2895f94f6e367dffb7ea0444d4cf7508 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:01:42 +0000 Subject: [PATCH 417/448] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 8fee23fa..0dd6d527 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 117 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-7988523e72ff96a93d2913e63ba27c5bb94af9893b94fceeeda64e0b43fb0663.yml -openapi_spec_hash: ce2ae9f9137d4c2bf947d446d00ebec8 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-c8f49d6ea6355b703fa0e6cce4277df8954b9730ce4cdd3af5900f37769b40d9.yml +openapi_spec_hash: ca03a797fb8e4cc79b6be4513f2fc6e7 config_hash: e0741f8035aea13f71e54e0fdb88eaa4 From 63cfd949f5d9c9f07af54ccc86d1af3a8ababf81 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:40:09 +0000 Subject: [PATCH 418/448] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 0dd6d527..a84f620b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 117 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-c8f49d6ea6355b703fa0e6cce4277df8954b9730ce4cdd3af5900f37769b40d9.yml -openapi_spec_hash: ca03a797fb8e4cc79b6be4513f2fc6e7 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-a42305a37f0b2f42afb552acc9a5294c210a5278eb6067df803abe36da45c5b9.yml +openapi_spec_hash: 4140db7dd999fee4b02e76dfb7d003b5 config_hash: e0741f8035aea13f71e54e0fdb88eaa4 From 4c1da72153e8d5bd72bdd9ffa2f40964161abbf0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 15:29:18 +0000 Subject: [PATCH 419/448] feat(api): move browser telemetry SSE stream to /browsers/{id}/telemetry/stream --- .stats.yml | 6 +++--- api.md | 2 +- src/kernel/resources/browsers/telemetry.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.stats.yml b/.stats.yml index a84f620b..5a5e5632 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 117 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-a42305a37f0b2f42afb552acc9a5294c210a5278eb6067df803abe36da45c5b9.yml -openapi_spec_hash: 4140db7dd999fee4b02e76dfb7d003b5 -config_hash: e0741f8035aea13f71e54e0fdb88eaa4 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-1acd8f0b76ab00e36b53cc3ca90b72b2199f3388b3e307890adb464b87f9a2d8.yml +openapi_spec_hash: 82003125c1c2c5d82d19270bafb4a6ca +config_hash: ede72e4ae65cc5a6d6927938b3455c46 diff --git a/api.md b/api.md index c9e1b290..a7fda470 100644 --- a/api.md +++ b/api.md @@ -143,7 +143,7 @@ from kernel.types.browsers import ( Methods: -- client.browsers.telemetry.stream(id) -> TelemetryStreamResponse +- client.browsers.telemetry.stream(id) -> TelemetryStreamResponse ## Replays diff --git a/src/kernel/resources/browsers/telemetry.py b/src/kernel/resources/browsers/telemetry.py index cb95b2d1..008b1d29 100644 --- a/src/kernel/resources/browsers/telemetry.py +++ b/src/kernel/resources/browsers/telemetry.py @@ -80,7 +80,7 @@ def stream( extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} extra_headers = {**strip_not_given({"Last-Event-ID": last_event_id}), **(extra_headers or {})} return self._get( - path_template("/browsers/{id}/telemetry", id=id), + path_template("/browsers/{id}/telemetry/stream", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -149,7 +149,7 @@ async def stream( extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} extra_headers = {**strip_not_given({"Last-Event-ID": last_event_id}), **(extra_headers or {})} return await self._get( - path_template("/browsers/{id}/telemetry", id=id), + path_template("/browsers/{id}/telemetry/stream", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), From 556784b3dd68a5be04847325023d94903c7f01d2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 15:35:59 +0000 Subject: [PATCH 420/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 2fbefb94..85c31182 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.58.0" + ".": "0.59.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 3ff0ba88..24264fbc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.58.0" +version = "0.59.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 9422de86..86fdb652 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.58.0" # x-release-please-version +__version__ = "0.59.0" # x-release-please-version From 3a6d842387d4761df266908a4837011e000afea9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:45:26 +0000 Subject: [PATCH 421/448] feat: Fix browser pool update schema --- .stats.yml | 4 ++-- src/kernel/resources/browser_pools.py | 24 +++++++++---------- .../types/browser_pool_update_params.py | 16 ++++++------- tests/api_resources/test_browser_pools.py | 12 ++-------- 4 files changed, 24 insertions(+), 32 deletions(-) diff --git a/.stats.yml b/.stats.yml index 5a5e5632..57627cd0 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 117 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-1acd8f0b76ab00e36b53cc3ca90b72b2199f3388b3e307890adb464b87f9a2d8.yml -openapi_spec_hash: 82003125c1c2c5d82d19270bafb4a6ca +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-2d1337eec44e036b9c896b7db4691f0a12edfa79d3f28b611818bcedf62d44ee.yml +openapi_spec_hash: 30110dbbe733b16e40a6d0aa41d0c8c4 config_hash: ede72e4ae65cc5a6d6927938b3455c46 diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py index db8ebbeb..64751271 100644 --- a/src/kernel/resources/browser_pools.py +++ b/src/kernel/resources/browser_pools.py @@ -206,7 +206,6 @@ def update( self, id_or_name: str, *, - size: int, chrome_policy: Dict[str, object] | Omit = omit, discard_all_idle: bool | Omit = omit, extensions: Iterable[BrowserExtension] | Omit = omit, @@ -216,6 +215,7 @@ def update( name: str | Omit = omit, profile: BrowserProfile | Omit = omit, proxy_id: str | Omit = omit, + size: int | Omit = omit, start_url: str | Omit = omit, stealth: bool | Omit = omit, timeout_seconds: int | Omit = omit, @@ -231,10 +231,6 @@ def update( Updates the configuration used to create browsers in the pool. Args: - size: Number of browsers to maintain in the pool. The maximum size is determined by - your organization's pooled sessions limit (the sum of all pool sizes cannot - exceed your limit). - chrome_policy: Custom Chrome enterprise policy overrides applied to all browsers in this pool. Keys are Chrome enterprise policy names; values must match their expected types. Blocked: kernel-managed policies (extensions, proxy, CDP/automation). See @@ -261,6 +257,10 @@ def update( proxy_id: Optional proxy to associate to the browser session. Must reference a proxy belonging to the caller's org. + size: Number of browsers to maintain in the pool. The maximum size is determined by + your organization's pooled sessions limit (the sum of all pool sizes cannot + exceed your limit). + start_url: Optional URL to navigate to when a new browser is warmed into the pool. Best-effort: failures to navigate do not fail pool fill. Only applied to newly-warmed browsers; browsers reused via release/acquire keep whatever URL the @@ -300,7 +300,6 @@ def update( path_template("/browser_pools/{id_or_name}", id_or_name=id_or_name), body=maybe_transform( { - "size": size, "chrome_policy": chrome_policy, "discard_all_idle": discard_all_idle, "extensions": extensions, @@ -310,6 +309,7 @@ def update( "name": name, "profile": profile, "proxy_id": proxy_id, + "size": size, "start_url": start_url, "stealth": stealth, "timeout_seconds": timeout_seconds, @@ -684,7 +684,6 @@ async def update( self, id_or_name: str, *, - size: int, chrome_policy: Dict[str, object] | Omit = omit, discard_all_idle: bool | Omit = omit, extensions: Iterable[BrowserExtension] | Omit = omit, @@ -694,6 +693,7 @@ async def update( name: str | Omit = omit, profile: BrowserProfile | Omit = omit, proxy_id: str | Omit = omit, + size: int | Omit = omit, start_url: str | Omit = omit, stealth: bool | Omit = omit, timeout_seconds: int | Omit = omit, @@ -709,10 +709,6 @@ async def update( Updates the configuration used to create browsers in the pool. Args: - size: Number of browsers to maintain in the pool. The maximum size is determined by - your organization's pooled sessions limit (the sum of all pool sizes cannot - exceed your limit). - chrome_policy: Custom Chrome enterprise policy overrides applied to all browsers in this pool. Keys are Chrome enterprise policy names; values must match their expected types. Blocked: kernel-managed policies (extensions, proxy, CDP/automation). See @@ -739,6 +735,10 @@ async def update( proxy_id: Optional proxy to associate to the browser session. Must reference a proxy belonging to the caller's org. + size: Number of browsers to maintain in the pool. The maximum size is determined by + your organization's pooled sessions limit (the sum of all pool sizes cannot + exceed your limit). + start_url: Optional URL to navigate to when a new browser is warmed into the pool. Best-effort: failures to navigate do not fail pool fill. Only applied to newly-warmed browsers; browsers reused via release/acquire keep whatever URL the @@ -778,7 +778,6 @@ async def update( path_template("/browser_pools/{id_or_name}", id_or_name=id_or_name), body=await async_maybe_transform( { - "size": size, "chrome_policy": chrome_policy, "discard_all_idle": discard_all_idle, "extensions": extensions, @@ -788,6 +787,7 @@ async def update( "name": name, "profile": profile, "proxy_id": proxy_id, + "size": size, "start_url": start_url, "stealth": stealth, "timeout_seconds": timeout_seconds, diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py index 4efc9ced..07501695 100644 --- a/src/kernel/types/browser_pool_update_params.py +++ b/src/kernel/types/browser_pool_update_params.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Dict, Iterable -from typing_extensions import Required, TypedDict +from typing_extensions import TypedDict from .shared_params.browser_profile import BrowserProfile from .shared_params.browser_viewport import BrowserViewport @@ -13,13 +13,6 @@ class BrowserPoolUpdateParams(TypedDict, total=False): - size: Required[int] - """Number of browsers to maintain in the pool. - - The maximum size is determined by your organization's pooled sessions limit (the - sum of all pool sizes cannot exceed your limit). - """ - chrome_policy: Dict[str, object] """Custom Chrome enterprise policy overrides applied to all browsers in this pool. @@ -68,6 +61,13 @@ class BrowserPoolUpdateParams(TypedDict, total=False): Must reference a proxy belonging to the caller's org. """ + size: int + """Number of browsers to maintain in the pool. + + The maximum size is determined by your organization's pooled sessions limit (the + sum of all pool sizes cannot exceed your limit). + """ + start_url: str """Optional URL to navigate to when a new browser is warmed into the pool. diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py index 959eeef2..42f47e63 100644 --- a/tests/api_resources/test_browser_pools.py +++ b/tests/api_resources/test_browser_pools.py @@ -135,7 +135,6 @@ def test_path_params_retrieve(self, client: Kernel) -> None: def test_method_update(self, client: Kernel) -> None: browser_pool = client.browser_pools.update( id_or_name="id_or_name", - size=10, ) assert_matches_type(BrowserPool, browser_pool, path=["response"]) @@ -144,7 +143,6 @@ def test_method_update(self, client: Kernel) -> None: def test_method_update_with_all_params(self, client: Kernel) -> None: browser_pool = client.browser_pools.update( id_or_name="id_or_name", - size=10, chrome_policy={"foo": "bar"}, discard_all_idle=False, extensions=[ @@ -163,6 +161,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None: "save_changes": True, }, proxy_id="proxy_id", + size=10, start_url="https://example.com", stealth=True, timeout_seconds=60, @@ -179,7 +178,6 @@ def test_method_update_with_all_params(self, client: Kernel) -> None: def test_raw_response_update(self, client: Kernel) -> None: response = client.browser_pools.with_raw_response.update( id_or_name="id_or_name", - size=10, ) assert response.is_closed is True @@ -192,7 +190,6 @@ def test_raw_response_update(self, client: Kernel) -> None: def test_streaming_response_update(self, client: Kernel) -> None: with client.browser_pools.with_streaming_response.update( id_or_name="id_or_name", - size=10, ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -208,7 +205,6 @@ def test_path_params_update(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): client.browser_pools.with_raw_response.update( id_or_name="", - size=10, ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -559,7 +555,6 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: async def test_method_update(self, async_client: AsyncKernel) -> None: browser_pool = await async_client.browser_pools.update( id_or_name="id_or_name", - size=10, ) assert_matches_type(BrowserPool, browser_pool, path=["response"]) @@ -568,7 +563,6 @@ async def test_method_update(self, async_client: AsyncKernel) -> None: async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> None: browser_pool = await async_client.browser_pools.update( id_or_name="id_or_name", - size=10, chrome_policy={"foo": "bar"}, discard_all_idle=False, extensions=[ @@ -587,6 +581,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> "save_changes": True, }, proxy_id="proxy_id", + size=10, start_url="https://example.com", stealth=True, timeout_seconds=60, @@ -603,7 +598,6 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> async def test_raw_response_update(self, async_client: AsyncKernel) -> None: response = await async_client.browser_pools.with_raw_response.update( id_or_name="id_or_name", - size=10, ) assert response.is_closed is True @@ -616,7 +610,6 @@ async def test_raw_response_update(self, async_client: AsyncKernel) -> None: async def test_streaming_response_update(self, async_client: AsyncKernel) -> None: async with async_client.browser_pools.with_streaming_response.update( id_or_name="id_or_name", - size=10, ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -632,7 +625,6 @@ async def test_path_params_update(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): await async_client.browser_pools.with_raw_response.update( id_or_name="", - size=10, ) @pytest.mark.skip(reason="Mock server tests are disabled") From f5301e599104d85526ac75aa85098c5cb83efbff Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 19:40:35 +0000 Subject: [PATCH 422/448] feat: Add API-backed API key management endpoints --- .stats.yml | 4 ++-- src/kernel/resources/api_keys.py | 27 +++++++++++++++++++++++++ src/kernel/types/api_key_list_params.py | 14 ++++++++++++- tests/api_resources/test_api_keys.py | 6 ++++++ 4 files changed, 48 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 57627cd0..9f173dc5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 117 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-2d1337eec44e036b9c896b7db4691f0a12edfa79d3f28b611818bcedf62d44ee.yml -openapi_spec_hash: 30110dbbe733b16e40a6d0aa41d0c8c4 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-3a1db7f11a92b28681929255ada59d2317ee4db98b5ff5aa6ce142a0664058b3.yml +openapi_spec_hash: b3064eaa589ae2a84993686ad1a3ee43 config_hash: ede72e4ae65cc5a6d6927938b3455c46 diff --git a/src/kernel/resources/api_keys.py b/src/kernel/resources/api_keys.py index aae96794..93e80158 100644 --- a/src/kernel/resources/api_keys.py +++ b/src/kernel/resources/api_keys.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Optional +from typing_extensions import Literal import httpx @@ -171,6 +172,9 @@ def list( *, limit: int | Omit = omit, offset: int | Omit = omit, + query: str | Omit = omit, + sort_by: Literal["created_at", "name", "expires_at"] | Omit = omit, + sort_direction: Literal["asc", "desc"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -187,6 +191,13 @@ def list( offset: Number of results to skip + query: Case-insensitive substring match against API key name, creator, and project. API + key identifiers and masked keys match by exact value or prefix. + + sort_by: Field to sort API keys by. + + sort_direction: Sort direction for API keys. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -207,6 +218,9 @@ def list( { "limit": limit, "offset": offset, + "query": query, + "sort_by": sort_by, + "sort_direction": sort_direction, }, api_key_list_params.APIKeyListParams, ), @@ -395,6 +409,9 @@ def list( *, limit: int | Omit = omit, offset: int | Omit = omit, + query: str | Omit = omit, + sort_by: Literal["created_at", "name", "expires_at"] | Omit = omit, + sort_direction: Literal["asc", "desc"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -411,6 +428,13 @@ def list( offset: Number of results to skip + query: Case-insensitive substring match against API key name, creator, and project. API + key identifiers and masked keys match by exact value or prefix. + + sort_by: Field to sort API keys by. + + sort_direction: Sort direction for API keys. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -431,6 +455,9 @@ def list( { "limit": limit, "offset": offset, + "query": query, + "sort_by": sort_by, + "sort_direction": sort_direction, }, api_key_list_params.APIKeyListParams, ), diff --git a/src/kernel/types/api_key_list_params.py b/src/kernel/types/api_key_list_params.py index e7d807f2..79a9c41c 100644 --- a/src/kernel/types/api_key_list_params.py +++ b/src/kernel/types/api_key_list_params.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing_extensions import TypedDict +from typing_extensions import Literal, TypedDict __all__ = ["APIKeyListParams"] @@ -13,3 +13,15 @@ class APIKeyListParams(TypedDict, total=False): offset: int """Number of results to skip""" + + query: str + """Case-insensitive substring match against API key name, creator, and project. + + API key identifiers and masked keys match by exact value or prefix. + """ + + sort_by: Literal["created_at", "name", "expires_at"] + """Field to sort API keys by.""" + + sort_direction: Literal["asc", "desc"] + """Sort direction for API keys.""" diff --git a/tests/api_resources/test_api_keys.py b/tests/api_resources/test_api_keys.py index da854b23..9c3abcae 100644 --- a/tests/api_resources/test_api_keys.py +++ b/tests/api_resources/test_api_keys.py @@ -162,6 +162,9 @@ def test_method_list_with_all_params(self, client: Kernel) -> None: api_key = client.api_keys.list( limit=100, offset=0, + query="query", + sort_by="created_at", + sort_direction="asc", ) assert_matches_type(SyncOffsetPagination[APIKey], api_key, path=["response"]) @@ -379,6 +382,9 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N api_key = await async_client.api_keys.list( limit=100, offset=0, + query="query", + sort_by="created_at", + sort_direction="asc", ) assert_matches_type(AsyncOffsetPagination[APIKey], api_key, path=["response"]) From 498dd2f1affeb4d91d873658410107d35e240fd2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 19:57:45 +0000 Subject: [PATCH 423/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 85c31182..284ce936 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.59.0" + ".": "0.60.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 24264fbc..ad5fcaee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.59.0" +version = "0.60.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 86fdb652..fdecb536 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.59.0" # x-release-please-version +__version__ = "0.60.0" # x-release-please-version From 75334d30eea130060c55e473855be80e39602432 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 20:39:29 +0000 Subject: [PATCH 424/448] feat: Add record_audio option to browser replay recording API --- .stats.yml | 4 ++-- src/kernel/resources/browsers/replays.py | 10 ++++++++++ src/kernel/types/browsers/replay_start_params.py | 6 ++++++ tests/api_resources/browsers/test_replays.py | 2 ++ 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 9f173dc5..27228661 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 117 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-3a1db7f11a92b28681929255ada59d2317ee4db98b5ff5aa6ce142a0664058b3.yml -openapi_spec_hash: b3064eaa589ae2a84993686ad1a3ee43 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-33e46e6a0095c2ec39a51860ee4e133c5a21a80a90cbe9e52953c07e5e0295de.yml +openapi_spec_hash: 4aa466b9af39768b65a44b68ae0d1f6e config_hash: ede72e4ae65cc5a6d6927938b3455c46 diff --git a/src/kernel/resources/browsers/replays.py b/src/kernel/resources/browsers/replays.py index 8f56ea5f..e58f8333 100644 --- a/src/kernel/resources/browsers/replays.py +++ b/src/kernel/resources/browsers/replays.py @@ -128,6 +128,7 @@ def start( *, framerate: int | Omit = omit, max_duration_in_seconds: int | Omit = omit, + record_audio: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -144,6 +145,9 @@ def start( max_duration_in_seconds: Maximum recording duration in seconds. + record_audio: Record audio in addition to video. When false (the default), the recording is + video-only. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -160,6 +164,7 @@ def start( { "framerate": framerate, "max_duration_in_seconds": max_duration_in_seconds, + "record_audio": record_audio, }, replay_start_params.ReplayStartParams, ), @@ -305,6 +310,7 @@ async def start( *, framerate: int | Omit = omit, max_duration_in_seconds: int | Omit = omit, + record_audio: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -321,6 +327,9 @@ async def start( max_duration_in_seconds: Maximum recording duration in seconds. + record_audio: Record audio in addition to video. When false (the default), the recording is + video-only. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -337,6 +346,7 @@ async def start( { "framerate": framerate, "max_duration_in_seconds": max_duration_in_seconds, + "record_audio": record_audio, }, replay_start_params.ReplayStartParams, ), diff --git a/src/kernel/types/browsers/replay_start_params.py b/src/kernel/types/browsers/replay_start_params.py index d77b6c49..c37c554d 100644 --- a/src/kernel/types/browsers/replay_start_params.py +++ b/src/kernel/types/browsers/replay_start_params.py @@ -16,3 +16,9 @@ class ReplayStartParams(TypedDict, total=False): max_duration_in_seconds: int """Maximum recording duration in seconds.""" + + record_audio: bool + """Record audio in addition to video. + + When false (the default), the recording is video-only. + """ diff --git a/tests/api_resources/browsers/test_replays.py b/tests/api_resources/browsers/test_replays.py index 72e986c1..0b450ff8 100644 --- a/tests/api_resources/browsers/test_replays.py +++ b/tests/api_resources/browsers/test_replays.py @@ -142,6 +142,7 @@ def test_method_start_with_all_params(self, client: Kernel) -> None: id="id", framerate=1, max_duration_in_seconds=1, + record_audio=True, ) assert_matches_type(ReplayStartResponse, replay, path=["response"]) @@ -354,6 +355,7 @@ async def test_method_start_with_all_params(self, async_client: AsyncKernel) -> id="id", framerate=1, max_duration_in_seconds=1, + record_audio=True, ) assert_matches_type(ReplayStartResponse, replay, path=["response"]) From 42208ed8ec052c1526559fff8e967dbfe634682a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 20:42:06 +0000 Subject: [PATCH 425/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 284ce936..f7e971ea 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.60.0" + ".": "0.61.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index ad5fcaee..2b9342ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.60.0" +version = "0.61.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index fdecb536..fc502359 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.60.0" # x-release-please-version +__version__ = "0.61.0" # x-release-please-version From 1d4961e82dc45ad373a6d6976a8c1d5beafc4c0e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 21:04:44 +0000 Subject: [PATCH 426/448] feat: api: paginate GET /proxies --- .stats.yml | 6 +- api.md | 2 +- src/kernel/resources/proxies.py | 83 +++++++++++++++++++++---- src/kernel/types/__init__.py | 1 + src/kernel/types/proxy_list_params.py | 15 +++++ src/kernel/types/proxy_list_response.py | 42 ++++++------- tests/api_resources/test_proxies.py | 31 +++++++-- 7 files changed, 134 insertions(+), 46 deletions(-) create mode 100644 src/kernel/types/proxy_list_params.py diff --git a/.stats.yml b/.stats.yml index 27228661..032ac8ce 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 117 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-33e46e6a0095c2ec39a51860ee4e133c5a21a80a90cbe9e52953c07e5e0295de.yml -openapi_spec_hash: 4aa466b9af39768b65a44b68ae0d1f6e -config_hash: ede72e4ae65cc5a6d6927938b3455c46 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-f3e52a8a9d9e37f8b527bdcce63b39cf8807d138cf3570c9954ba7f42ef02375.yml +openapi_spec_hash: 2b787691eba1bb7bafb553528c09d5c0 +config_hash: e12afec516ced9520590c7a05af7a6f7 diff --git a/api.md b/api.md index a7fda470..1cc2b82f 100644 --- a/api.md +++ b/api.md @@ -324,7 +324,7 @@ Methods: - client.proxies.create(\*\*params) -> ProxyCreateResponse - client.proxies.retrieve(id) -> ProxyRetrieveResponse -- client.proxies.list() -> ProxyListResponse +- client.proxies.list(\*\*params) -> SyncOffsetPagination[ProxyListResponse] - client.proxies.delete(id) -> None - client.proxies.check(id, \*\*params) -> ProxyCheckResponse diff --git a/src/kernel/resources/proxies.py b/src/kernel/resources/proxies.py index 501a2e88..2239a854 100644 --- a/src/kernel/resources/proxies.py +++ b/src/kernel/resources/proxies.py @@ -6,7 +6,7 @@ import httpx -from ..types import proxy_check_params, proxy_create_params +from ..types import proxy_list_params, proxy_check_params, proxy_create_params from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, SequenceNotStr, omit, not_given from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property @@ -17,7 +17,8 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from .._base_client import make_request_options +from ..pagination import SyncOffsetPagination, AsyncOffsetPagination +from .._base_client import AsyncPaginator, make_request_options from ..types.proxy_list_response import ProxyListResponse from ..types.proxy_check_response import ProxyCheckResponse from ..types.proxy_create_response import ProxyCreateResponse @@ -140,20 +141,48 @@ def retrieve( def list( self, *, + limit: int | Omit = omit, + offset: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ProxyListResponse: - """List proxies owned by the caller's organization.""" - return self._get( + ) -> SyncOffsetPagination[ProxyListResponse]: + """ + List proxies owned by the caller's organization. + + Args: + limit: Limit the number of proxies to return. + + offset: Offset the number of proxies to return. + + 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_api_list( "/proxies", + page=SyncOffsetPagination[ProxyListResponse], options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "offset": offset, + }, + proxy_list_params.ProxyListParams, + ), ), - cast_to=ProxyListResponse, + model=ProxyListResponse, ) def delete( @@ -357,23 +386,51 @@ async def retrieve( cast_to=ProxyRetrieveResponse, ) - async def list( + def list( self, *, + limit: int | Omit = omit, + offset: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ProxyListResponse: - """List proxies owned by the caller's organization.""" - return await self._get( + ) -> AsyncPaginator[ProxyListResponse, AsyncOffsetPagination[ProxyListResponse]]: + """ + List proxies owned by the caller's organization. + + Args: + limit: Limit the number of proxies to return. + + offset: Offset the number of proxies to return. + + 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_api_list( "/proxies", + page=AsyncOffsetPagination[ProxyListResponse], options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "offset": offset, + }, + proxy_list_params.ProxyListParams, + ), ), - cast_to=ProxyListResponse, + model=ProxyListResponse, ) async def delete( diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index fb6fd589..bdc65fdf 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -25,6 +25,7 @@ from .created_api_key import CreatedAPIKey as CreatedAPIKey from .browser_pool_ref import BrowserPoolRef as BrowserPoolRef from .app_list_response import AppListResponse as AppListResponse +from .proxy_list_params import ProxyListParams as ProxyListParams from .proxy_check_params import ProxyCheckParams as ProxyCheckParams from .api_key_list_params import APIKeyListParams as APIKeyListParams from .browser_curl_params import BrowserCurlParams as BrowserCurlParams diff --git a/src/kernel/types/proxy_list_params.py b/src/kernel/types/proxy_list_params.py new file mode 100644 index 00000000..78972bcb --- /dev/null +++ b/src/kernel/types/proxy_list_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["ProxyListParams"] + + +class ProxyListParams(TypedDict, total=False): + limit: int + """Limit the number of proxies to return.""" + + offset: int + """Offset the number of proxies to return.""" diff --git a/src/kernel/types/proxy_list_response.py b/src/kernel/types/proxy_list_response.py index d1ddc079..6d40a8a4 100644 --- a/src/kernel/types/proxy_list_response.py +++ b/src/kernel/types/proxy_list_response.py @@ -8,31 +8,30 @@ __all__ = [ "ProxyListResponse", - "ProxyListResponseItem", - "ProxyListResponseItemConfig", - "ProxyListResponseItemConfigDatacenterProxyConfig", - "ProxyListResponseItemConfigIspProxyConfig", - "ProxyListResponseItemConfigResidentialProxyConfig", - "ProxyListResponseItemConfigMobileProxyConfig", - "ProxyListResponseItemConfigCustomProxyConfig", + "Config", + "ConfigDatacenterProxyConfig", + "ConfigIspProxyConfig", + "ConfigResidentialProxyConfig", + "ConfigMobileProxyConfig", + "ConfigCustomProxyConfig", ] -class ProxyListResponseItemConfigDatacenterProxyConfig(BaseModel): +class ConfigDatacenterProxyConfig(BaseModel): """Configuration for a datacenter proxy.""" country: Optional[str] = None """ISO 3166 country code. Defaults to US if not provided.""" -class ProxyListResponseItemConfigIspProxyConfig(BaseModel): +class ConfigIspProxyConfig(BaseModel): """Configuration for an ISP proxy.""" country: Optional[str] = None """ISO 3166 country code. Defaults to US if not provided.""" -class ProxyListResponseItemConfigResidentialProxyConfig(BaseModel): +class ConfigResidentialProxyConfig(BaseModel): """Configuration for residential proxies.""" asn: Optional[str] = None @@ -57,7 +56,7 @@ class ProxyListResponseItemConfigResidentialProxyConfig(BaseModel): """US ZIP code.""" -class ProxyListResponseItemConfigMobileProxyConfig(BaseModel): +class ConfigMobileProxyConfig(BaseModel): """Configuration for mobile proxies.""" city: Optional[str] = None @@ -70,7 +69,7 @@ class ProxyListResponseItemConfigMobileProxyConfig(BaseModel): """US-only state code. Mobile carrier routing can make observed geo vary.""" -class ProxyListResponseItemConfigCustomProxyConfig(BaseModel): +class ConfigCustomProxyConfig(BaseModel): """Configuration for a custom proxy (e.g., private proxy server).""" host: str @@ -86,16 +85,16 @@ class ProxyListResponseItemConfigCustomProxyConfig(BaseModel): """Username for proxy authentication.""" -ProxyListResponseItemConfig: TypeAlias = Union[ - ProxyListResponseItemConfigDatacenterProxyConfig, - ProxyListResponseItemConfigIspProxyConfig, - ProxyListResponseItemConfigResidentialProxyConfig, - ProxyListResponseItemConfigMobileProxyConfig, - ProxyListResponseItemConfigCustomProxyConfig, +Config: TypeAlias = Union[ + ConfigDatacenterProxyConfig, + ConfigIspProxyConfig, + ConfigResidentialProxyConfig, + ConfigMobileProxyConfig, + ConfigCustomProxyConfig, ] -class ProxyListResponseItem(BaseModel): +class ProxyListResponse(BaseModel): """Configuration for routing traffic through a proxy.""" type: Literal["datacenter", "isp", "residential", "mobile", "custom"] @@ -110,7 +109,7 @@ class ProxyListResponseItem(BaseModel): bypass_hosts: Optional[List[str]] = None """Hostnames that should bypass the parent proxy and connect directly.""" - config: Optional[ProxyListResponseItemConfig] = None + config: Optional[Config] = None """Configuration specific to the selected proxy `type`.""" ip_address: Optional[str] = None @@ -127,6 +126,3 @@ class ProxyListResponseItem(BaseModel): status: Optional[Literal["available", "unavailable"]] = None """Current health status of the proxy.""" - - -ProxyListResponse: TypeAlias = List[ProxyListResponseItem] diff --git a/tests/api_resources/test_proxies.py b/tests/api_resources/test_proxies.py index fd8080ee..64cb90b9 100644 --- a/tests/api_resources/test_proxies.py +++ b/tests/api_resources/test_proxies.py @@ -15,6 +15,7 @@ ProxyCreateResponse, ProxyRetrieveResponse, ) +from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -114,7 +115,16 @@ def test_path_params_retrieve(self, client: Kernel) -> None: @parametrize def test_method_list(self, client: Kernel) -> None: proxy = client.proxies.list() - assert_matches_type(ProxyListResponse, proxy, path=["response"]) + assert_matches_type(SyncOffsetPagination[ProxyListResponse], proxy, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_with_all_params(self, client: Kernel) -> None: + proxy = client.proxies.list( + limit=1, + offset=0, + ) + assert_matches_type(SyncOffsetPagination[ProxyListResponse], proxy, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -124,7 +134,7 @@ def test_raw_response_list(self, client: Kernel) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" proxy = response.parse() - assert_matches_type(ProxyListResponse, proxy, path=["response"]) + assert_matches_type(SyncOffsetPagination[ProxyListResponse], proxy, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -134,7 +144,7 @@ def test_streaming_response_list(self, client: Kernel) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" proxy = response.parse() - assert_matches_type(ProxyListResponse, proxy, path=["response"]) + assert_matches_type(SyncOffsetPagination[ProxyListResponse], proxy, path=["response"]) assert cast(Any, response.is_closed) is True @@ -329,7 +339,16 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: @parametrize async def test_method_list(self, async_client: AsyncKernel) -> None: proxy = await async_client.proxies.list() - assert_matches_type(ProxyListResponse, proxy, path=["response"]) + assert_matches_type(AsyncOffsetPagination[ProxyListResponse], proxy, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: + proxy = await async_client.proxies.list( + limit=1, + offset=0, + ) + assert_matches_type(AsyncOffsetPagination[ProxyListResponse], proxy, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -339,7 +358,7 @@ async def test_raw_response_list(self, async_client: AsyncKernel) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" proxy = await response.parse() - assert_matches_type(ProxyListResponse, proxy, path=["response"]) + assert_matches_type(AsyncOffsetPagination[ProxyListResponse], proxy, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -349,7 +368,7 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" proxy = await response.parse() - assert_matches_type(ProxyListResponse, proxy, path=["response"]) + assert_matches_type(AsyncOffsetPagination[ProxyListResponse], proxy, path=["response"]) assert cast(Any, response.is_closed) is True From 529aad8eedf9ed7a80f9ece742bdf6c2fb185e07 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 21:54:03 +0000 Subject: [PATCH 427/448] feat: api: paginate GET /browser_pools --- .stats.yml | 6 +- api.md | 4 +- src/kernel/resources/browser_pools.py | 83 ++++++++++++++++--- src/kernel/types/__init__.py | 2 +- src/kernel/types/browser_pool_list_params.py | 15 ++++ .../types/browser_pool_list_response.py | 10 --- tests/api_resources/test_browser_pools.py | 32 +++++-- 7 files changed, 116 insertions(+), 36 deletions(-) create mode 100644 src/kernel/types/browser_pool_list_params.py delete mode 100644 src/kernel/types/browser_pool_list_response.py diff --git a/.stats.yml b/.stats.yml index 032ac8ce..d6e38f27 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 117 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-f3e52a8a9d9e37f8b527bdcce63b39cf8807d138cf3570c9954ba7f42ef02375.yml -openapi_spec_hash: 2b787691eba1bb7bafb553528c09d5c0 -config_hash: e12afec516ced9520590c7a05af7a6f7 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e5524e6eeeedc614c05bf19896f1ce6c4f8a113de85c02952d6874d579651f70.yml +openapi_spec_hash: 7815a189a50e6aa9e283213cc78d041c +config_hash: 453b25a35834fd59a55a2315d1d50746 diff --git a/api.md b/api.md index 1cc2b82f..8a6b8c6e 100644 --- a/api.md +++ b/api.md @@ -349,7 +349,7 @@ Methods: Types: ```python -from kernel.types import BrowserPool, BrowserPoolListResponse, BrowserPoolAcquireResponse +from kernel.types import BrowserPool, BrowserPoolAcquireResponse ``` Methods: @@ -357,7 +357,7 @@ Methods: - client.browser_pools.create(\*\*params) -> BrowserPool - client.browser_pools.retrieve(id_or_name) -> BrowserPool - client.browser_pools.update(id_or_name, \*\*params) -> BrowserPool -- client.browser_pools.list() -> BrowserPoolListResponse +- client.browser_pools.list(\*\*params) -> SyncOffsetPagination[BrowserPool] - client.browser_pools.delete(id_or_name, \*\*params) -> None - client.browser_pools.acquire(id_or_name, \*\*params) -> BrowserPoolAcquireResponse - client.browser_pools.flush(id_or_name) -> None diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py index 64751271..203e22ad 100644 --- a/src/kernel/resources/browser_pools.py +++ b/src/kernel/resources/browser_pools.py @@ -7,6 +7,7 @@ import httpx from ..types import ( + browser_pool_list_params, browser_pool_create_params, browser_pool_delete_params, browser_pool_update_params, @@ -23,9 +24,9 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from .._base_client import make_request_options +from ..pagination import SyncOffsetPagination, AsyncOffsetPagination +from .._base_client import AsyncPaginator, make_request_options from ..types.browser_pool import BrowserPool -from ..types.browser_pool_list_response import BrowserPoolListResponse from ..types.browser_pool_acquire_response import BrowserPoolAcquireResponse from ..types.shared_params.browser_profile import BrowserProfile from ..types.shared_params.browser_viewport import BrowserViewport @@ -326,20 +327,48 @@ def update( def list( self, *, + limit: int | Omit = omit, + offset: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> BrowserPoolListResponse: - """List browser pools owned by the caller's organization.""" - return self._get( + ) -> SyncOffsetPagination[BrowserPool]: + """ + List browser pools owned by the caller's organization. + + Args: + limit: Limit the number of browser pools to return. + + offset: Offset the number of browser pools to return. + + 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_api_list( "/browser_pools", + page=SyncOffsetPagination[BrowserPool], options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "offset": offset, + }, + browser_pool_list_params.BrowserPoolListParams, + ), ), - cast_to=BrowserPoolListResponse, + model=BrowserPool, ) def delete( @@ -801,23 +830,51 @@ async def update( cast_to=BrowserPool, ) - async def list( + def list( self, *, + limit: int | Omit = omit, + offset: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> BrowserPoolListResponse: - """List browser pools owned by the caller's organization.""" - return await self._get( + ) -> AsyncPaginator[BrowserPool, AsyncOffsetPagination[BrowserPool]]: + """ + List browser pools owned by the caller's organization. + + Args: + limit: Limit the number of browser pools to return. + + offset: Offset the number of browser pools to return. + + 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_api_list( "/browser_pools", + page=AsyncOffsetPagination[BrowserPool], options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "offset": offset, + }, + browser_pool_list_params.BrowserPoolListParams, + ), ), - cast_to=BrowserPoolListResponse, + model=BrowserPool, ) async def delete( diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index bdc65fdf..ae16742d 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -57,6 +57,7 @@ from .extension_list_response import ExtensionListResponse as ExtensionListResponse from .extension_upload_params import ExtensionUploadParams as ExtensionUploadParams from .proxy_retrieve_response import ProxyRetrieveResponse as ProxyRetrieveResponse +from .browser_pool_list_params import BrowserPoolListParams as BrowserPoolListParams from .credential_create_params import CredentialCreateParams as CredentialCreateParams from .credential_provider_item import CredentialProviderItem as CredentialProviderItem from .credential_update_params import CredentialUpdateParams as CredentialUpdateParams @@ -71,7 +72,6 @@ from .extension_upload_response import ExtensionUploadResponse as ExtensionUploadResponse from .browser_pool_create_params import BrowserPoolCreateParams as BrowserPoolCreateParams from .browser_pool_delete_params import BrowserPoolDeleteParams as BrowserPoolDeleteParams -from .browser_pool_list_response import BrowserPoolListResponse as BrowserPoolListResponse from .browser_pool_update_params import BrowserPoolUpdateParams as BrowserPoolUpdateParams from .deployment_create_response import DeploymentCreateResponse as DeploymentCreateResponse from .deployment_follow_response import DeploymentFollowResponse as DeploymentFollowResponse diff --git a/src/kernel/types/browser_pool_list_params.py b/src/kernel/types/browser_pool_list_params.py new file mode 100644 index 00000000..f8111e3e --- /dev/null +++ b/src/kernel/types/browser_pool_list_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["BrowserPoolListParams"] + + +class BrowserPoolListParams(TypedDict, total=False): + limit: int + """Limit the number of browser pools to return.""" + + offset: int + """Offset the number of browser pools to return.""" diff --git a/src/kernel/types/browser_pool_list_response.py b/src/kernel/types/browser_pool_list_response.py deleted file mode 100644 index a11c4de2..00000000 --- a/src/kernel/types/browser_pool_list_response.py +++ /dev/null @@ -1,10 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List -from typing_extensions import TypeAlias - -from .browser_pool import BrowserPool - -__all__ = ["BrowserPoolListResponse"] - -BrowserPoolListResponse: TypeAlias = List[BrowserPool] diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py index 42f47e63..c71e6c27 100644 --- a/tests/api_resources/test_browser_pools.py +++ b/tests/api_resources/test_browser_pools.py @@ -11,9 +11,9 @@ from tests.utils import assert_matches_type from kernel.types import ( BrowserPool, - BrowserPoolListResponse, BrowserPoolAcquireResponse, ) +from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -211,7 +211,16 @@ def test_path_params_update(self, client: Kernel) -> None: @parametrize def test_method_list(self, client: Kernel) -> None: browser_pool = client.browser_pools.list() - assert_matches_type(BrowserPoolListResponse, browser_pool, path=["response"]) + assert_matches_type(SyncOffsetPagination[BrowserPool], browser_pool, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_with_all_params(self, client: Kernel) -> None: + browser_pool = client.browser_pools.list( + limit=1, + offset=0, + ) + assert_matches_type(SyncOffsetPagination[BrowserPool], browser_pool, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -221,7 +230,7 @@ def test_raw_response_list(self, client: Kernel) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" browser_pool = response.parse() - assert_matches_type(BrowserPoolListResponse, browser_pool, path=["response"]) + assert_matches_type(SyncOffsetPagination[BrowserPool], browser_pool, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -231,7 +240,7 @@ def test_streaming_response_list(self, client: Kernel) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" browser_pool = response.parse() - assert_matches_type(BrowserPoolListResponse, browser_pool, path=["response"]) + assert_matches_type(SyncOffsetPagination[BrowserPool], browser_pool, path=["response"]) assert cast(Any, response.is_closed) is True @@ -631,7 +640,16 @@ async def test_path_params_update(self, async_client: AsyncKernel) -> None: @parametrize async def test_method_list(self, async_client: AsyncKernel) -> None: browser_pool = await async_client.browser_pools.list() - assert_matches_type(BrowserPoolListResponse, browser_pool, path=["response"]) + assert_matches_type(AsyncOffsetPagination[BrowserPool], browser_pool, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: + browser_pool = await async_client.browser_pools.list( + limit=1, + offset=0, + ) + assert_matches_type(AsyncOffsetPagination[BrowserPool], browser_pool, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -641,7 +659,7 @@ async def test_raw_response_list(self, async_client: AsyncKernel) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" browser_pool = await response.parse() - assert_matches_type(BrowserPoolListResponse, browser_pool, path=["response"]) + assert_matches_type(AsyncOffsetPagination[BrowserPool], browser_pool, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -651,7 +669,7 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" browser_pool = await response.parse() - assert_matches_type(BrowserPoolListResponse, browser_pool, path=["response"]) + assert_matches_type(AsyncOffsetPagination[BrowserPool], browser_pool, path=["response"]) assert cast(Any, response.is_closed) is True From 60f34703715fcfd896c9380d3ccd7513e97fba00 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:29:10 +0000 Subject: [PATCH 428/448] feat: api: paginate GET /extensions --- .stats.yml | 6 +- api.md | 2 +- src/kernel/resources/extensions.py | 83 +++++++++++++++++---- src/kernel/types/__init__.py | 1 + src/kernel/types/extension_list_params.py | 15 ++++ src/kernel/types/extension_list_response.py | 10 +-- tests/api_resources/test_extensions.py | 31 ++++++-- 7 files changed, 118 insertions(+), 30 deletions(-) create mode 100644 src/kernel/types/extension_list_params.py diff --git a/.stats.yml b/.stats.yml index d6e38f27..582e6b44 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 117 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e5524e6eeeedc614c05bf19896f1ce6c4f8a113de85c02952d6874d579651f70.yml -openapi_spec_hash: 7815a189a50e6aa9e283213cc78d041c -config_hash: 453b25a35834fd59a55a2315d1d50746 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-9a76e7a837ac9ffbe0e9557d618ce9b676e052249e364e516c28ddb33b912019.yml +openapi_spec_hash: c0e5412cc95139f11a8b8635559bb985 +config_hash: 098a7f3fd134c8e4cbd3a44750892a18 diff --git a/api.md b/api.md index 8a6b8c6e..6001d530 100644 --- a/api.md +++ b/api.md @@ -338,7 +338,7 @@ from kernel.types import ExtensionListResponse, ExtensionUploadResponse Methods: -- client.extensions.list() -> ExtensionListResponse +- client.extensions.list(\*\*params) -> SyncOffsetPagination[ExtensionListResponse] - client.extensions.delete(id_or_name) -> None - client.extensions.download(id_or_name) -> BinaryAPIResponse - client.extensions.download_from_chrome_store(\*\*params) -> BinaryAPIResponse diff --git a/src/kernel/resources/extensions.py b/src/kernel/resources/extensions.py index d5cf0eb7..5b7215be 100644 --- a/src/kernel/resources/extensions.py +++ b/src/kernel/resources/extensions.py @@ -7,7 +7,7 @@ import httpx -from ..types import extension_upload_params, extension_download_from_chrome_store_params +from ..types import extension_list_params, extension_upload_params, extension_download_from_chrome_store_params from .._files import deepcopy_with_paths from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, FileTypes, omit, not_given from .._utils import extract_files, path_template, maybe_transform, async_maybe_transform @@ -27,7 +27,8 @@ async_to_custom_raw_response_wrapper, async_to_custom_streamed_response_wrapper, ) -from .._base_client import make_request_options +from ..pagination import SyncOffsetPagination, AsyncOffsetPagination +from .._base_client import AsyncPaginator, make_request_options from ..types.extension_list_response import ExtensionListResponse from ..types.extension_upload_response import ExtensionUploadResponse @@ -59,20 +60,48 @@ def with_streaming_response(self) -> ExtensionsResourceWithStreamingResponse: def list( self, *, + limit: int | Omit = omit, + offset: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ExtensionListResponse: - """List extensions owned by the caller's organization.""" - return self._get( + ) -> SyncOffsetPagination[ExtensionListResponse]: + """ + List extensions owned by the caller's organization. + + Args: + limit: Limit the number of extensions to return. + + offset: Offset the number of extensions to return. + + 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_api_list( "/extensions", + page=SyncOffsetPagination[ExtensionListResponse], options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "offset": offset, + }, + extension_list_params.ExtensionListParams, + ), ), - cast_to=ExtensionListResponse, + model=ExtensionListResponse, ) def delete( @@ -266,23 +295,51 @@ def with_streaming_response(self) -> AsyncExtensionsResourceWithStreamingRespons """ return AsyncExtensionsResourceWithStreamingResponse(self) - async def list( + def list( self, *, + limit: int | Omit = omit, + offset: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ExtensionListResponse: - """List extensions owned by the caller's organization.""" - return await self._get( + ) -> AsyncPaginator[ExtensionListResponse, AsyncOffsetPagination[ExtensionListResponse]]: + """ + List extensions owned by the caller's organization. + + Args: + limit: Limit the number of extensions to return. + + offset: Offset the number of extensions to return. + + 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_api_list( "/extensions", + page=AsyncOffsetPagination[ExtensionListResponse], options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "offset": offset, + }, + extension_list_params.ExtensionListParams, + ), ), - cast_to=ExtensionListResponse, + model=ExtensionListResponse, ) async def delete( diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index ae16742d..ef27f493 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -42,6 +42,7 @@ from .browser_curl_response import BrowserCurlResponse as BrowserCurlResponse from .browser_list_response import BrowserListResponse as BrowserListResponse from .browser_update_params import BrowserUpdateParams as BrowserUpdateParams +from .extension_list_params import ExtensionListParams as ExtensionListParams from .profile_create_params import ProfileCreateParams as ProfileCreateParams from .project_create_params import ProjectCreateParams as ProjectCreateParams from .project_update_params import ProjectUpdateParams as ProjectUpdateParams diff --git a/src/kernel/types/extension_list_params.py b/src/kernel/types/extension_list_params.py new file mode 100644 index 00000000..bccd61eb --- /dev/null +++ b/src/kernel/types/extension_list_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["ExtensionListParams"] + + +class ExtensionListParams(TypedDict, total=False): + limit: int + """Limit the number of extensions to return.""" + + offset: int + """Offset the number of extensions to return.""" diff --git a/src/kernel/types/extension_list_response.py b/src/kernel/types/extension_list_response.py index bf9e544d..6bb33561 100644 --- a/src/kernel/types/extension_list_response.py +++ b/src/kernel/types/extension_list_response.py @@ -1,15 +1,14 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List, Optional +from typing import Optional from datetime import datetime -from typing_extensions import TypeAlias from .._models import BaseModel -__all__ = ["ExtensionListResponse", "ExtensionListResponseItem"] +__all__ = ["ExtensionListResponse"] -class ExtensionListResponseItem(BaseModel): +class ExtensionListResponse(BaseModel): """A browser extension uploaded to Kernel.""" id: str @@ -29,6 +28,3 @@ class ExtensionListResponseItem(BaseModel): Must be unique within the project. """ - - -ExtensionListResponse: TypeAlias = List[ExtensionListResponseItem] diff --git a/tests/api_resources/test_extensions.py b/tests/api_resources/test_extensions.py index 4a28fb5e..302ba958 100644 --- a/tests/api_resources/test_extensions.py +++ b/tests/api_resources/test_extensions.py @@ -21,6 +21,7 @@ StreamedBinaryAPIResponse, AsyncStreamedBinaryAPIResponse, ) +from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -32,7 +33,16 @@ class TestExtensions: @parametrize def test_method_list(self, client: Kernel) -> None: extension = client.extensions.list() - assert_matches_type(ExtensionListResponse, extension, path=["response"]) + assert_matches_type(SyncOffsetPagination[ExtensionListResponse], extension, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_with_all_params(self, client: Kernel) -> None: + extension = client.extensions.list( + limit=1, + offset=0, + ) + assert_matches_type(SyncOffsetPagination[ExtensionListResponse], extension, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -42,7 +52,7 @@ def test_raw_response_list(self, client: Kernel) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = response.parse() - assert_matches_type(ExtensionListResponse, extension, path=["response"]) + assert_matches_type(SyncOffsetPagination[ExtensionListResponse], extension, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -52,7 +62,7 @@ def test_streaming_response_list(self, client: Kernel) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = response.parse() - assert_matches_type(ExtensionListResponse, extension, path=["response"]) + assert_matches_type(SyncOffsetPagination[ExtensionListResponse], extension, path=["response"]) assert cast(Any, response.is_closed) is True @@ -256,7 +266,16 @@ class TestAsyncExtensions: @parametrize async def test_method_list(self, async_client: AsyncKernel) -> None: extension = await async_client.extensions.list() - assert_matches_type(ExtensionListResponse, extension, path=["response"]) + assert_matches_type(AsyncOffsetPagination[ExtensionListResponse], extension, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: + extension = await async_client.extensions.list( + limit=1, + offset=0, + ) + assert_matches_type(AsyncOffsetPagination[ExtensionListResponse], extension, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -266,7 +285,7 @@ async def test_raw_response_list(self, async_client: AsyncKernel) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = await response.parse() - assert_matches_type(ExtensionListResponse, extension, path=["response"]) + assert_matches_type(AsyncOffsetPagination[ExtensionListResponse], extension, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -276,7 +295,7 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" extension = await response.parse() - assert_matches_type(ExtensionListResponse, extension, path=["response"]) + assert_matches_type(AsyncOffsetPagination[ExtensionListResponse], extension, path=["response"]) assert cast(Any, response.is_closed) is True From e849f5261b575be9942492fcafce6db343ae4748 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 15:04:06 +0000 Subject: [PATCH 429/448] feat: api: paginate GET /org/credential_providers --- .stats.yml | 6 +- api.md | 3 +- src/kernel/resources/credential_providers.py | 88 ++++++++++++++++--- src/kernel/types/__init__.py | 2 +- .../types/credential_provider_list_params.py | 15 ++++ .../credential_provider_list_response.py | 10 --- .../test_credential_providers.py | 32 +++++-- 7 files changed, 119 insertions(+), 37 deletions(-) create mode 100644 src/kernel/types/credential_provider_list_params.py delete mode 100644 src/kernel/types/credential_provider_list_response.py diff --git a/.stats.yml b/.stats.yml index 582e6b44..5ea72073 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 117 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-9a76e7a837ac9ffbe0e9557d618ce9b676e052249e364e516c28ddb33b912019.yml -openapi_spec_hash: c0e5412cc95139f11a8b8635559bb985 -config_hash: 098a7f3fd134c8e4cbd3a44750892a18 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e6c711f0d29a7d956cc8ca621440da966c1f1575b1205d01328b1099edf1c517.yml +openapi_spec_hash: c06e7e36de1c6f9b29b54a6e3dc08ee5 +config_hash: 36159c262d293fbeacf513ab600a1729 diff --git a/api.md b/api.md index 6001d530..8f9df345 100644 --- a/api.md +++ b/api.md @@ -441,7 +441,6 @@ from kernel.types import ( CredentialProviderItem, CredentialProviderTestResult, UpdateCredentialProviderRequest, - CredentialProviderListResponse, CredentialProviderListItemsResponse, ) ``` @@ -451,7 +450,7 @@ Methods: - client.credential_providers.create(\*\*params) -> CredentialProvider - client.credential_providers.retrieve(id) -> CredentialProvider - client.credential_providers.update(id, \*\*params) -> CredentialProvider -- client.credential_providers.list() -> CredentialProviderListResponse +- client.credential_providers.list(\*\*params) -> SyncOffsetPagination[CredentialProvider] - client.credential_providers.delete(id) -> None - client.credential_providers.list_items(id) -> CredentialProviderListItemsResponse - client.credential_providers.test(id) -> CredentialProviderTestResult diff --git a/src/kernel/resources/credential_providers.py b/src/kernel/resources/credential_providers.py index 2dede2c4..06bf2aee 100644 --- a/src/kernel/resources/credential_providers.py +++ b/src/kernel/resources/credential_providers.py @@ -6,7 +6,11 @@ import httpx -from ..types import credential_provider_create_params, credential_provider_update_params +from ..types import ( + credential_provider_list_params, + credential_provider_create_params, + credential_provider_update_params, +) from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property @@ -17,10 +21,10 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from .._base_client import make_request_options +from ..pagination import SyncOffsetPagination, AsyncOffsetPagination +from .._base_client import AsyncPaginator, make_request_options from ..types.credential_provider import CredentialProvider from ..types.credential_provider_test_result import CredentialProviderTestResult -from ..types.credential_provider_list_response import CredentialProviderListResponse from ..types.credential_provider_list_items_response import CredentialProviderListItemsResponse __all__ = ["CredentialProvidersResource", "AsyncCredentialProvidersResource"] @@ -194,20 +198,48 @@ def update( def list( self, *, + limit: int | Omit = omit, + offset: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> CredentialProviderListResponse: - """List external credential providers configured for the organization.""" - return self._get( + ) -> SyncOffsetPagination[CredentialProvider]: + """ + List external credential providers configured for the organization. + + Args: + limit: Limit the number of credential providers to return. + + offset: Offset the number of credential providers to return. + + 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_api_list( "/org/credential_providers", + page=SyncOffsetPagination[CredentialProvider], options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "offset": offset, + }, + credential_provider_list_params.CredentialProviderListParams, + ), ), - cast_to=CredentialProviderListResponse, + model=CredentialProvider, ) def delete( @@ -477,23 +509,51 @@ async def update( cast_to=CredentialProvider, ) - async def list( + def list( self, *, + limit: int | Omit = omit, + offset: int | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> CredentialProviderListResponse: - """List external credential providers configured for the organization.""" - return await self._get( + ) -> AsyncPaginator[CredentialProvider, AsyncOffsetPagination[CredentialProvider]]: + """ + List external credential providers configured for the organization. + + Args: + limit: Limit the number of credential providers to return. + + offset: Offset the number of credential providers to return. + + 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_api_list( "/org/credential_providers", + page=AsyncOffsetPagination[CredentialProvider], options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "offset": offset, + }, + credential_provider_list_params.CredentialProviderListParams, + ), ), - cast_to=CredentialProviderListResponse, + model=CredentialProvider, ) async def delete( diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index ef27f493..7932162d 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -86,9 +86,9 @@ from .browser_pool_acquire_response import BrowserPoolAcquireResponse as BrowserPoolAcquireResponse from .credential_totp_code_response import CredentialTotpCodeResponse as CredentialTotpCodeResponse from .browser_load_extensions_params import BrowserLoadExtensionsParams as BrowserLoadExtensionsParams +from .credential_provider_list_params import CredentialProviderListParams as CredentialProviderListParams from .credential_provider_test_result import CredentialProviderTestResult as CredentialProviderTestResult from .credential_provider_create_params import CredentialProviderCreateParams as CredentialProviderCreateParams -from .credential_provider_list_response import CredentialProviderListResponse as CredentialProviderListResponse from .credential_provider_update_params import CredentialProviderUpdateParams as CredentialProviderUpdateParams from .invocation_list_browsers_response import InvocationListBrowsersResponse as InvocationListBrowsersResponse from .credential_provider_list_items_response import ( diff --git a/src/kernel/types/credential_provider_list_params.py b/src/kernel/types/credential_provider_list_params.py new file mode 100644 index 00000000..87dbfd69 --- /dev/null +++ b/src/kernel/types/credential_provider_list_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["CredentialProviderListParams"] + + +class CredentialProviderListParams(TypedDict, total=False): + limit: int + """Limit the number of credential providers to return.""" + + offset: int + """Offset the number of credential providers to return.""" diff --git a/src/kernel/types/credential_provider_list_response.py b/src/kernel/types/credential_provider_list_response.py deleted file mode 100644 index 59814e03..00000000 --- a/src/kernel/types/credential_provider_list_response.py +++ /dev/null @@ -1,10 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List -from typing_extensions import TypeAlias - -from .credential_provider import CredentialProvider - -__all__ = ["CredentialProviderListResponse"] - -CredentialProviderListResponse: TypeAlias = List[CredentialProvider] diff --git a/tests/api_resources/test_credential_providers.py b/tests/api_resources/test_credential_providers.py index 93f44145..d9795332 100644 --- a/tests/api_resources/test_credential_providers.py +++ b/tests/api_resources/test_credential_providers.py @@ -12,9 +12,9 @@ from kernel.types import ( CredentialProvider, CredentialProviderTestResult, - CredentialProviderListResponse, CredentialProviderListItemsResponse, ) +from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -174,7 +174,16 @@ def test_path_params_update(self, client: Kernel) -> None: @parametrize def test_method_list(self, client: Kernel) -> None: credential_provider = client.credential_providers.list() - assert_matches_type(CredentialProviderListResponse, credential_provider, path=["response"]) + assert_matches_type(SyncOffsetPagination[CredentialProvider], credential_provider, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_with_all_params(self, client: Kernel) -> None: + credential_provider = client.credential_providers.list( + limit=1, + offset=0, + ) + assert_matches_type(SyncOffsetPagination[CredentialProvider], credential_provider, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -184,7 +193,7 @@ def test_raw_response_list(self, client: Kernel) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" credential_provider = response.parse() - assert_matches_type(CredentialProviderListResponse, credential_provider, path=["response"]) + assert_matches_type(SyncOffsetPagination[CredentialProvider], credential_provider, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -194,7 +203,7 @@ def test_streaming_response_list(self, client: Kernel) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" credential_provider = response.parse() - assert_matches_type(CredentialProviderListResponse, credential_provider, path=["response"]) + assert_matches_type(SyncOffsetPagination[CredentialProvider], credential_provider, path=["response"]) assert cast(Any, response.is_closed) is True @@ -482,7 +491,16 @@ async def test_path_params_update(self, async_client: AsyncKernel) -> None: @parametrize async def test_method_list(self, async_client: AsyncKernel) -> None: credential_provider = await async_client.credential_providers.list() - assert_matches_type(CredentialProviderListResponse, credential_provider, path=["response"]) + assert_matches_type(AsyncOffsetPagination[CredentialProvider], credential_provider, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: + credential_provider = await async_client.credential_providers.list( + limit=1, + offset=0, + ) + assert_matches_type(AsyncOffsetPagination[CredentialProvider], credential_provider, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -492,7 +510,7 @@ async def test_raw_response_list(self, async_client: AsyncKernel) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" credential_provider = await response.parse() - assert_matches_type(CredentialProviderListResponse, credential_provider, path=["response"]) + assert_matches_type(AsyncOffsetPagination[CredentialProvider], credential_provider, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -502,7 +520,7 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" credential_provider = await response.parse() - assert_matches_type(CredentialProviderListResponse, credential_provider, path=["response"]) + assert_matches_type(AsyncOffsetPagination[CredentialProvider], credential_provider, path=["response"]) assert cast(Any, response.is_closed) is True From 282fd73c02b258662403653bbc56030118a38323 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 15:40:06 +0000 Subject: [PATCH 430/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index f7e971ea..fdce8724 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.61.0" + ".": "0.62.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 2b9342ba..f246f34a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.61.0" +version = "0.62.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index fc502359..785c4450 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.61.0" # x-release-please-version +__version__ = "0.62.0" # x-release-please-version From 6199c3f3f976c73118fcd32e5c1ee7c20eb500c5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 19:39:12 +0000 Subject: [PATCH 431/448] feat(api): allow setting a custom name on a browser session at create time --- .stats.yml | 4 ++-- src/kernel/resources/browsers/browsers.py | 16 ++++++++++++++-- src/kernel/types/browser_create_params.py | 7 +++++++ src/kernel/types/browser_create_response.py | 3 +++ src/kernel/types/browser_list_params.py | 2 +- src/kernel/types/browser_list_response.py | 3 +++ .../types/browser_pool_acquire_response.py | 3 +++ src/kernel/types/browser_retrieve_response.py | 3 +++ src/kernel/types/browser_update_response.py | 3 +++ .../types/invocation_list_browsers_response.py | 3 +++ tests/api_resources/test_browsers.py | 2 ++ 11 files changed, 44 insertions(+), 5 deletions(-) diff --git a/.stats.yml b/.stats.yml index 5ea72073..b8e42864 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 117 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e6c711f0d29a7d956cc8ca621440da966c1f1575b1205d01328b1099edf1c517.yml -openapi_spec_hash: c06e7e36de1c6f9b29b54a6e3dc08ee5 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e7925f5cb9626a47fd24470851ff4149c27bc89795f75e8641c618fff93a076c.yml +openapi_spec_hash: 955d4bf81e9838b0732c0e52716a2030 config_hash: 36159c262d293fbeacf513ab600a1729 diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index f2c388dc..eafe69ff 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -162,6 +162,7 @@ def create( headless: bool | Omit = omit, invocation_id: str | Omit = omit, kiosk_mode: bool | Omit = omit, + name: str | Omit = omit, profile: BrowserProfile | Omit = omit, proxy_id: str | Omit = omit, start_url: str | Omit = omit, @@ -198,6 +199,10 @@ def create( kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live view. + name: Optional human-readable name for the browser session, used to find it later in + the dashboard. Must be unique among active sessions within the project. Set at + creation time only. + profile: Profile selection for the browser session. Provide either id or name. If specified, the matching profile will be loaded into the browser session. Profiles must be created beforehand. @@ -255,6 +260,7 @@ def create( "headless": headless, "invocation_id": invocation_id, "kiosk_mode": kiosk_mode, + "name": name, "profile": profile, "proxy_id": proxy_id, "start_url": start_url, @@ -406,7 +412,7 @@ def list( offset: Number of results to skip. Defaults to 0. - query: Search browsers by session ID, profile ID, proxy ID, or pool name. + query: Search browsers by name, session ID, profile ID, proxy ID, or pool name. status: Filter sessions by status. "active" returns only active sessions (default), "deleted" returns only soft-deleted sessions, "all" returns both. @@ -652,6 +658,7 @@ async def create( headless: bool | Omit = omit, invocation_id: str | Omit = omit, kiosk_mode: bool | Omit = omit, + name: str | Omit = omit, profile: BrowserProfile | Omit = omit, proxy_id: str | Omit = omit, start_url: str | Omit = omit, @@ -688,6 +695,10 @@ async def create( kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live view. + name: Optional human-readable name for the browser session, used to find it later in + the dashboard. Must be unique among active sessions within the project. Set at + creation time only. + profile: Profile selection for the browser session. Provide either id or name. If specified, the matching profile will be loaded into the browser session. Profiles must be created beforehand. @@ -745,6 +756,7 @@ async def create( "headless": headless, "invocation_id": invocation_id, "kiosk_mode": kiosk_mode, + "name": name, "profile": profile, "proxy_id": proxy_id, "start_url": start_url, @@ -896,7 +908,7 @@ def list( offset: Number of results to skip. Defaults to 0. - query: Search browsers by session ID, profile ID, proxy ID, or pool name. + query: Search browsers by name, session ID, profile ID, proxy ID, or pool name. status: Filter sessions by status. "active" returns only active sessions (default), "deleted" returns only soft-deleted sessions, "all" returns both. diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index c94071cf..de6ba5dd 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -49,6 +49,13 @@ class BrowserCreateParams(TypedDict, total=False): view. """ + name: str + """ + Optional human-readable name for the browser session, used to find it later in + the dashboard. Must be unique among active sessions within the project. Set at + creation time only. + """ + profile: BrowserProfile """Profile selection for the browser session. diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index 651216ab..27da0664 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -63,6 +63,9 @@ class BrowserCreateResponse(BaseModel): kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" + name: Optional[str] = None + """Human-readable name of the browser session, if one was set at creation.""" + pool: Optional[BrowserPoolRef] = None """Browser pool this session was acquired from, if any.""" diff --git a/src/kernel/types/browser_list_params.py b/src/kernel/types/browser_list_params.py index d659b8cb..2761e83b 100644 --- a/src/kernel/types/browser_list_params.py +++ b/src/kernel/types/browser_list_params.py @@ -22,7 +22,7 @@ class BrowserListParams(TypedDict, total=False): """Number of results to skip. Defaults to 0.""" query: str - """Search browsers by session ID, profile ID, proxy ID, or pool name.""" + """Search browsers by name, session ID, profile ID, proxy ID, or pool name.""" status: Literal["active", "deleted", "all"] """Filter sessions by status. diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index 79db0cfc..83154a94 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -63,6 +63,9 @@ class BrowserListResponse(BaseModel): kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" + name: Optional[str] = None + """Human-readable name of the browser session, if one was set at creation.""" + pool: Optional[BrowserPoolRef] = None """Browser pool this session was acquired from, if any.""" diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py index b3cbabfa..bcda928d 100644 --- a/src/kernel/types/browser_pool_acquire_response.py +++ b/src/kernel/types/browser_pool_acquire_response.py @@ -63,6 +63,9 @@ class BrowserPoolAcquireResponse(BaseModel): kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" + name: Optional[str] = None + """Human-readable name of the browser session, if one was set at creation.""" + pool: Optional[BrowserPoolRef] = None """Browser pool this session was acquired from, if any.""" diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 86b5fbef..9042d010 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -63,6 +63,9 @@ class BrowserRetrieveResponse(BaseModel): kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" + name: Optional[str] = None + """Human-readable name of the browser session, if one was set at creation.""" + pool: Optional[BrowserPoolRef] = None """Browser pool this session was acquired from, if any.""" diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py index 60e95176..89c79f48 100644 --- a/src/kernel/types/browser_update_response.py +++ b/src/kernel/types/browser_update_response.py @@ -63,6 +63,9 @@ class BrowserUpdateResponse(BaseModel): kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" + name: Optional[str] = None + """Human-readable name of the browser session, if one was set at creation.""" + pool: Optional[BrowserPoolRef] = None """Browser pool this session was acquired from, if any.""" diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py index 0a1700c2..d42a3abf 100644 --- a/src/kernel/types/invocation_list_browsers_response.py +++ b/src/kernel/types/invocation_list_browsers_response.py @@ -63,6 +63,9 @@ class Browser(BaseModel): kiosk_mode: Optional[bool] = None """Whether the browser session is running in kiosk mode.""" + name: Optional[str] = None + """Human-readable name of the browser session, if one was set at creation.""" + pool: Optional[BrowserPoolRef] = None """Browser pool this session was acquired from, if any.""" diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index c845e52b..69b14ac4 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -45,6 +45,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: headless=False, invocation_id="rr33xuugxj9h0bkf1rdt2bet", kiosk_mode=True, + name="amazon-scrape-1", profile={ "id": "id", "name": "name", @@ -451,6 +452,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> headless=False, invocation_id="rr33xuugxj9h0bkf1rdt2bet", kiosk_mode=True, + name="amazon-scrape-1", profile={ "id": "id", "name": "name", From 4c05ff507fd80fcc4fc927a232cb78752e26d0cb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 22:07:37 +0000 Subject: [PATCH 432/448] docs(api): use neutral example for browser session name field --- .stats.yml | 4 ++-- tests/api_resources/test_browsers.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.stats.yml b/.stats.yml index b8e42864..afee3b79 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 117 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e7925f5cb9626a47fd24470851ff4149c27bc89795f75e8641c618fff93a076c.yml -openapi_spec_hash: 955d4bf81e9838b0732c0e52716a2030 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-152175b971564fc089733419adff5f79aa5556cb264bea7be5ca7582fda82b61.yml +openapi_spec_hash: 054e48b4743d014f0ec199757990e2c3 config_hash: 36159c262d293fbeacf513ab600a1729 diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index 69b14ac4..06d997c6 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -45,7 +45,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: headless=False, invocation_id="rr33xuugxj9h0bkf1rdt2bet", kiosk_mode=True, - name="amazon-scrape-1", + name="checkout-flow-1", profile={ "id": "id", "name": "name", @@ -452,7 +452,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> headless=False, invocation_id="rr33xuugxj9h0bkf1rdt2bet", kiosk_mode=True, - name="amazon-scrape-1", + name="checkout-flow-1", profile={ "id": "id", "name": "name", From 926c2e63d089f5099209b505a7e58160026c914d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 22:18:56 +0000 Subject: [PATCH 433/448] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index afee3b79..c2258b3d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 117 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-152175b971564fc089733419adff5f79aa5556cb264bea7be5ca7582fda82b61.yml -openapi_spec_hash: 054e48b4743d014f0ec199757990e2c3 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-d0f7d8820ee342b6b8e32d5c08620d7a31135a6aabbe8266118b54269fbabb9d.yml +openapi_spec_hash: 371eb419c25eb160fbc0135e45c133d1 config_hash: 36159c262d293fbeacf513ab600a1729 From 7ae07cacffdff6bc9d86bffa7eb844edd760683d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 15:36:40 +0000 Subject: [PATCH 434/448] feat(api): allow setting key-value tags on a browser session at create time --- .stats.yml | 6 ++--- api.md | 1 + src/kernel/resources/browsers/browsers.py | 23 +++++++++++++++++++ src/kernel/types/__init__.py | 2 ++ src/kernel/types/browser_create_params.py | 7 ++++++ src/kernel/types/browser_create_response.py | 7 ++++++ src/kernel/types/browser_list_params.py | 8 +++++++ src/kernel/types/browser_list_response.py | 7 ++++++ .../types/browser_pool_acquire_response.py | 7 ++++++ src/kernel/types/browser_retrieve_response.py | 7 ++++++ src/kernel/types/browser_update_response.py | 7 ++++++ .../invocation_list_browsers_response.py | 7 ++++++ src/kernel/types/tags.py | 8 +++++++ src/kernel/types/tags_param.py | 10 ++++++++ tests/api_resources/test_browsers.py | 10 ++++++++ 15 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 src/kernel/types/tags.py create mode 100644 src/kernel/types/tags_param.py diff --git a/.stats.yml b/.stats.yml index c2258b3d..b53121ed 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 117 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-d0f7d8820ee342b6b8e32d5c08620d7a31135a6aabbe8266118b54269fbabb9d.yml -openapi_spec_hash: 371eb419c25eb160fbc0135e45c133d1 -config_hash: 36159c262d293fbeacf513ab600a1729 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-8f84f4214a8024d8ea62ee639eeaf2fa26900fabe23c8b87accb41d529a0bf4f.yml +openapi_spec_hash: db14f415438b3d338d9376bddc83a5cf +config_hash: 590bf8cb85948cf1e63b7b5ef60686c8 diff --git a/api.md b/api.md index 8f9df345..fe9135b5 100644 --- a/api.md +++ b/api.md @@ -83,6 +83,7 @@ from kernel.types import ( BrowserPoolRef, BrowserUsage, Profile, + Tags, BrowserCreateResponse, BrowserRetrieveResponse, BrowserUpdateResponse, diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index eafe69ff..ad570816 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -84,6 +84,7 @@ ) from ...pagination import SyncOffsetPagination, AsyncOffsetPagination from ..._base_client import AsyncPaginator, make_request_options +from ...types.tags_param import TagsParam from ...types.browser_curl_response import BrowserCurlResponse from ...types.browser_list_response import BrowserListResponse from ...types.browser_create_response import BrowserCreateResponse @@ -167,6 +168,7 @@ def create( proxy_id: str | Omit = omit, start_url: str | Omit = omit, stealth: bool | Omit = omit, + tags: TagsParam | Omit = omit, telemetry: Optional[browser_create_params.Telemetry] | Omit = omit, timeout_seconds: int | Omit = omit, viewport: BrowserViewport | Omit = omit, @@ -217,6 +219,9 @@ def create( stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. + tags: Optional user-defined key-value tags for the browser session, used to find and + group sessions later. Set at creation time only. Up to 50 pairs. + telemetry: Telemetry configuration for the browser session. Set enabled to true to start capture using VM defaults, or provide browser category settings. If omitted, null, set to an empty object ({}), set to enabled: false without browser @@ -265,6 +270,7 @@ def create( "proxy_id": proxy_id, "start_url": start_url, "stealth": stealth, + "tags": tags, "telemetry": telemetry, "timeout_seconds": timeout_seconds, "viewport": viewport, @@ -392,6 +398,7 @@ def list( offset: int | Omit = omit, query: str | Omit = omit, status: Literal["active", "deleted", "all"] | Omit = omit, + tags: Dict[str, str] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -417,6 +424,10 @@ def list( status: Filter sessions by status. "active" returns only active sessions (default), "deleted" returns only soft-deleted sessions, "all" returns both. + tags: Filter sessions by tag key-value pairs using deepObject style, e.g. + ?tags[team]=backend&tags[env]=staging. Multiple pairs are ANDed: a session must + match every supplied pair exactly. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -440,6 +451,7 @@ def list( "offset": offset, "query": query, "status": status, + "tags": tags, }, browser_list_params.BrowserListParams, ), @@ -663,6 +675,7 @@ async def create( proxy_id: str | Omit = omit, start_url: str | Omit = omit, stealth: bool | Omit = omit, + tags: TagsParam | Omit = omit, telemetry: Optional[browser_create_params.Telemetry] | Omit = omit, timeout_seconds: int | Omit = omit, viewport: BrowserViewport | Omit = omit, @@ -713,6 +726,9 @@ async def create( stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. + tags: Optional user-defined key-value tags for the browser session, used to find and + group sessions later. Set at creation time only. Up to 50 pairs. + telemetry: Telemetry configuration for the browser session. Set enabled to true to start capture using VM defaults, or provide browser category settings. If omitted, null, set to an empty object ({}), set to enabled: false without browser @@ -761,6 +777,7 @@ async def create( "proxy_id": proxy_id, "start_url": start_url, "stealth": stealth, + "tags": tags, "telemetry": telemetry, "timeout_seconds": timeout_seconds, "viewport": viewport, @@ -888,6 +905,7 @@ def list( offset: int | Omit = omit, query: str | Omit = omit, status: Literal["active", "deleted", "all"] | Omit = omit, + tags: Dict[str, str] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -913,6 +931,10 @@ def list( status: Filter sessions by status. "active" returns only active sessions (default), "deleted" returns only soft-deleted sessions, "all" returns both. + tags: Filter sessions by tag key-value pairs using deepObject style, e.g. + ?tags[team]=backend&tags[env]=staging. Multiple pairs are ANDed: a session must + match every supplied pair exactly. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -936,6 +958,7 @@ def list( "offset": offset, "query": query, "status": status, + "tags": tags, }, browser_list_params.BrowserListParams, ), diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 7932162d..1e9b39a4 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -4,6 +4,7 @@ from . import browsers from .. import _compat +from .tags import Tags as Tags from .shared import ( LogEvent as LogEvent, AppAction as AppAction, @@ -19,6 +20,7 @@ from .profile import Profile as Profile from .project import Project as Project from .credential import Credential as Credential +from .tags_param import TagsParam as TagsParam from .browser_pool import BrowserPool as BrowserPool from .browser_usage import BrowserUsage as BrowserUsage from .app_list_params import AppListParams as AppListParams diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index de6ba5dd..fe62f31a 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -5,6 +5,7 @@ from typing import Dict, Iterable, Optional from typing_extensions import TypedDict +from .tags_param import TagsParam from .shared_params.browser_profile import BrowserProfile from .shared_params.browser_viewport import BrowserViewport from .shared_params.browser_extension import BrowserExtension @@ -82,6 +83,12 @@ class BrowserCreateParams(TypedDict, total=False): mechanisms. """ + tags: TagsParam + """ + Optional user-defined key-value tags for the browser session, used to find and + group sessions later. Set at creation time only. Up to 50 pairs. + """ + telemetry: Optional[Telemetry] """Telemetry configuration for the browser session. diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index 27da0664..ec54e598 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -3,6 +3,7 @@ from typing import Dict, Optional from datetime import datetime +from .tags import Tags from .profile import Profile from .._models import BaseModel from .browser_usage import BrowserUsage @@ -84,6 +85,12 @@ class BrowserCreateResponse(BaseModel): browser actually loaded. """ + tags: Optional[Tags] = None + """User-defined key-value tags that were set on this browser session, if any. + + Echoed back when present. + """ + telemetry: Optional[BrowserTelemetryConfig] = None """Active telemetry configuration for the session, if any.""" diff --git a/src/kernel/types/browser_list_params.py b/src/kernel/types/browser_list_params.py index 2761e83b..4006dfbe 100644 --- a/src/kernel/types/browser_list_params.py +++ b/src/kernel/types/browser_list_params.py @@ -2,6 +2,7 @@ from __future__ import annotations +from typing import Dict from typing_extensions import Literal, TypedDict __all__ = ["BrowserListParams"] @@ -30,3 +31,10 @@ class BrowserListParams(TypedDict, total=False): "active" returns only active sessions (default), "deleted" returns only soft-deleted sessions, "all" returns both. """ + + tags: Dict[str, str] + """Filter sessions by tag key-value pairs using deepObject style, e.g. + + ?tags[team]=backend&tags[env]=staging. Multiple pairs are ANDed: a session must + match every supplied pair exactly. + """ diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index 83154a94..4de6bb00 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -3,6 +3,7 @@ from typing import Dict, Optional from datetime import datetime +from .tags import Tags from .profile import Profile from .._models import BaseModel from .browser_usage import BrowserUsage @@ -84,6 +85,12 @@ class BrowserListResponse(BaseModel): browser actually loaded. """ + tags: Optional[Tags] = None + """User-defined key-value tags that were set on this browser session, if any. + + Echoed back when present. + """ + telemetry: Optional[BrowserTelemetryConfig] = None """Active telemetry configuration for the session, if any.""" diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py index bcda928d..6d3b0b68 100644 --- a/src/kernel/types/browser_pool_acquire_response.py +++ b/src/kernel/types/browser_pool_acquire_response.py @@ -3,6 +3,7 @@ from typing import Dict, Optional from datetime import datetime +from .tags import Tags from .profile import Profile from .._models import BaseModel from .browser_usage import BrowserUsage @@ -84,6 +85,12 @@ class BrowserPoolAcquireResponse(BaseModel): browser actually loaded. """ + tags: Optional[Tags] = None + """User-defined key-value tags that were set on this browser session, if any. + + Echoed back when present. + """ + telemetry: Optional[BrowserTelemetryConfig] = None """Active telemetry configuration for the session, if any.""" diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 9042d010..48dfa362 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -3,6 +3,7 @@ from typing import Dict, Optional from datetime import datetime +from .tags import Tags from .profile import Profile from .._models import BaseModel from .browser_usage import BrowserUsage @@ -84,6 +85,12 @@ class BrowserRetrieveResponse(BaseModel): browser actually loaded. """ + tags: Optional[Tags] = None + """User-defined key-value tags that were set on this browser session, if any. + + Echoed back when present. + """ + telemetry: Optional[BrowserTelemetryConfig] = None """Active telemetry configuration for the session, if any.""" diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py index 89c79f48..15c043ea 100644 --- a/src/kernel/types/browser_update_response.py +++ b/src/kernel/types/browser_update_response.py @@ -3,6 +3,7 @@ from typing import Dict, Optional from datetime import datetime +from .tags import Tags from .profile import Profile from .._models import BaseModel from .browser_usage import BrowserUsage @@ -84,6 +85,12 @@ class BrowserUpdateResponse(BaseModel): browser actually loaded. """ + tags: Optional[Tags] = None + """User-defined key-value tags that were set on this browser session, if any. + + Echoed back when present. + """ + telemetry: Optional[BrowserTelemetryConfig] = None """Active telemetry configuration for the session, if any.""" diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py index d42a3abf..b060f8bb 100644 --- a/src/kernel/types/invocation_list_browsers_response.py +++ b/src/kernel/types/invocation_list_browsers_response.py @@ -3,6 +3,7 @@ from typing import Dict, List, Optional from datetime import datetime +from .tags import Tags from .profile import Profile from .._models import BaseModel from .browser_usage import BrowserUsage @@ -84,6 +85,12 @@ class Browser(BaseModel): browser actually loaded. """ + tags: Optional[Tags] = None + """User-defined key-value tags that were set on this browser session, if any. + + Echoed back when present. + """ + telemetry: Optional[BrowserTelemetryConfig] = None """Active telemetry configuration for the session, if any.""" diff --git a/src/kernel/types/tags.py b/src/kernel/types/tags.py new file mode 100644 index 00000000..988894da --- /dev/null +++ b/src/kernel/types/tags.py @@ -0,0 +1,8 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict +from typing_extensions import TypeAlias + +__all__ = ["Tags"] + +Tags: TypeAlias = Dict[str, str] diff --git a/src/kernel/types/tags_param.py b/src/kernel/types/tags_param.py new file mode 100644 index 00000000..9e9e5cba --- /dev/null +++ b/src/kernel/types/tags_param.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict +from typing_extensions import TypeAlias + +__all__ = ["TagsParam"] + +TagsParam: TypeAlias = Dict[str, str] diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index 06d997c6..590640c9 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -54,6 +54,10 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: proxy_id="proxy_id", start_url="https://example.com", stealth=True, + tags={ + "team": "backend", + "env": "staging", + }, telemetry={ "browser": { "console": {"enabled": True}, @@ -232,6 +236,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None: offset=0, query="query", status="active", + tags={"foo": "string"}, ) assert_matches_type(SyncOffsetPagination[BrowserListResponse], browser, path=["response"]) @@ -461,6 +466,10 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> proxy_id="proxy_id", start_url="https://example.com", stealth=True, + tags={ + "team": "backend", + "env": "staging", + }, telemetry={ "browser": { "console": {"enabled": True}, @@ -639,6 +648,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N offset=0, query="query", status="active", + tags={"foo": "string"}, ) assert_matches_type(AsyncOffsetPagination[BrowserListResponse], browser, path=["response"]) From 985dd9bc85aeb1ed2288223b3f25e0578d32b4d6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 17:25:20 +0000 Subject: [PATCH 435/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index fdce8724..3cb6257c 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.62.0" + ".": "0.63.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index f246f34a..710e7a06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.62.0" +version = "0.63.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 785c4450..52d2f619 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.62.0" # x-release-please-version +__version__ = "0.63.0" # x-release-please-version From a6d5f85f575bbb3cd16d83f6289b6b987a9a0f72 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 18:06:21 +0000 Subject: [PATCH 436/448] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index b53121ed..8991baf8 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 117 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-8f84f4214a8024d8ea62ee639eeaf2fa26900fabe23c8b87accb41d529a0bf4f.yml -openapi_spec_hash: db14f415438b3d338d9376bddc83a5cf +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-53de38ec04ee49846c89946b2fe72641c5ee005956d1fff4e8a50de9ba22e874.yml +openapi_spec_hash: 96162b6a1e4a2521be1b6c4fb9b0e5ce config_hash: 590bf8cb85948cf1e63b7b5ef60686c8 From 7bdae421500473a82ac5556a6944c423ddff2154 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 21:37:42 +0000 Subject: [PATCH 437/448] feat: Telemetry: expose opt-in categories + full event taxonomy (public API) --- .stats.yml | 6 +- api.md | 8 ++ src/kernel/types/browser_create_params.py | 15 +-- src/kernel/types/browser_update_params.py | 15 +-- src/kernel/types/browsers/__init__.py | 8 ++ .../types/browsers/browser_api_call_event.py | 42 ++++++++ .../browser_captcha_solve_result_event.py | 63 ++++++++++++ .../browsers/browser_cdp_connect_event.py | 29 ++++++ .../browsers/browser_cdp_disconnect_event.py | 49 +++++++++ .../browser_live_view_connect_event.py | 40 ++++++++ .../browser_live_view_disconnect_event.py | 41 ++++++++ .../browser_monitor_disconnected_event.py | 2 +- .../browser_monitor_init_failed_event.py | 2 +- .../browser_monitor_reconnect_failed_event.py | 2 +- .../browser_monitor_reconnected_event.py | 2 +- .../browser_monitor_screenshot_event.py | 2 +- .../browsers/browser_service_crashed_event.py | 50 ++++++++++ .../browsers/browser_system_oom_kill_event.py | 99 +++++++++++++++++++ .../browser_telemetry_categories_config.py | 45 ++++++++- ...owser_telemetry_categories_config_param.py | 45 ++++++++- .../browser_telemetry_category_config.py | 5 +- ...browser_telemetry_category_config_param.py | 5 +- .../types/browsers/browser_telemetry_event.py | 16 +++ .../browsers/telemetry_stream_response.py | 12 ++- tests/api_resources/test_browsers.py | 20 ++++ 25 files changed, 584 insertions(+), 39 deletions(-) create mode 100644 src/kernel/types/browsers/browser_api_call_event.py create mode 100644 src/kernel/types/browsers/browser_captcha_solve_result_event.py create mode 100644 src/kernel/types/browsers/browser_cdp_connect_event.py create mode 100644 src/kernel/types/browsers/browser_cdp_disconnect_event.py create mode 100644 src/kernel/types/browsers/browser_live_view_connect_event.py create mode 100644 src/kernel/types/browsers/browser_live_view_disconnect_event.py create mode 100644 src/kernel/types/browsers/browser_service_crashed_event.py create mode 100644 src/kernel/types/browsers/browser_system_oom_kill_event.py diff --git a/.stats.yml b/.stats.yml index 8991baf8..d9e97532 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 117 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-53de38ec04ee49846c89946b2fe72641c5ee005956d1fff4e8a50de9ba22e874.yml -openapi_spec_hash: 96162b6a1e4a2521be1b6c4fb9b0e5ce -config_hash: 590bf8cb85948cf1e63b7b5ef60686c8 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-9d489e3e43edfa64a754d4281241718e01c85d9a82ef3687df12bbd3c4ff5b42.yml +openapi_spec_hash: a953cafb7f40ec8495dbd7df8bab8bad +config_hash: bb7acce8576a50dd449b0c8f58ef0f1d diff --git a/api.md b/api.md index fe9135b5..c5019550 100644 --- a/api.md +++ b/api.md @@ -108,7 +108,11 @@ Types: ```python from kernel.types.browsers import ( + BrowserAPICallEvent, BrowserCallStack, + BrowserCaptchaSolveResultEvent, + BrowserCdpConnectEvent, + BrowserCdpDisconnectEvent, BrowserConsoleErrorEvent, BrowserConsoleLogEvent, BrowserEventContext, @@ -117,6 +121,8 @@ from kernel.types.browsers import ( BrowserInteractionClickEvent, BrowserInteractionKeyEvent, BrowserInteractionScrollSettledEvent, + BrowserLiveViewConnectEvent, + BrowserLiveViewDisconnectEvent, BrowserMonitorDisconnectedEvent, BrowserMonitorInitFailedEvent, BrowserMonitorReconnectFailedEvent, @@ -134,6 +140,8 @@ from kernel.types.browsers import ( BrowserPageNavigationEvent, BrowserPageNavigationSettledEvent, BrowserPageTabOpenedEvent, + BrowserServiceCrashedEvent, + BrowserSystemOomKillEvent, BrowserTelemetryCategoriesConfig, BrowserTelemetryCategoryConfig, BrowserTelemetryConfig, diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index fe62f31a..6d7b80b3 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -131,17 +131,18 @@ class Telemetry(TypedDict, total=False): """ browser: BrowserTelemetryCategoriesConfigParam - """Per-category enable/disable flags. + """Per-category capture flags. - If enabled is true and browser is omitted or empty, the VM default category set - is used. Explicitly disabling all four categories stops capture on update and - starts no capture on create. + Selection is opt-in: only the categories set to enabled=true are captured; + anything omitted is off. If enabled is true and browser is omitted or empty, the + default category set is used. A browser config that enables nothing stops + capture on update and starts no capture on create. """ enabled: bool """Request shortcut for browser telemetry capture. - True enables capture using VM defaults unless browser category settings are - provided. False stops capture on update and starts no capture on create. - enabled=false cannot be combined with browser category settings. + True enables capture using the default category set unless browser category + settings are provided. False stops capture on update and starts no capture on + create. enabled=false cannot be combined with browser category settings. """ diff --git a/src/kernel/types/browser_update_params.py b/src/kernel/types/browser_update_params.py index 837c1784..2626917c 100644 --- a/src/kernel/types/browser_update_params.py +++ b/src/kernel/types/browser_update_params.py @@ -52,19 +52,20 @@ class Telemetry(TypedDict, total=False): """ browser: BrowserTelemetryCategoriesConfigParam - """Per-category enable/disable flags. + """Per-category capture flags. - If enabled is true and browser is omitted or empty, the VM default category set - is used. Explicitly disabling all four categories stops capture on update and - starts no capture on create. + Selection is opt-in: only the categories set to enabled=true are captured; + anything omitted is off. If enabled is true and browser is omitted or empty, the + default category set is used. A browser config that enables nothing stops + capture on update and starts no capture on create. """ enabled: bool """Request shortcut for browser telemetry capture. - True enables capture using VM defaults unless browser category settings are - provided. False stops capture on update and starts no capture on create. - enabled=false cannot be combined with browser category settings. + True enables capture using the default category set unless browser category + settings are provided. False stops capture on update and starts no capture on + create. enabled=false cannot be combined with browser category settings. """ diff --git a/src/kernel/types/browsers/__init__.py b/src/kernel/types/browsers/__init__.py index 34f4fd64..5f14ecd1 100644 --- a/src/kernel/types/browsers/__init__.py +++ b/src/kernel/types/browsers/__init__.py @@ -28,6 +28,7 @@ from .process_kill_response import ProcessKillResponse as ProcessKillResponse from .process_resize_params import ProcessResizeParams as ProcessResizeParams from .replay_start_response import ReplayStartResponse as ReplayStartResponse +from .browser_api_call_event import BrowserAPICallEvent as BrowserAPICallEvent from .browser_page_lcp_event import BrowserPageLcpEvent as BrowserPageLcpEvent from .computer_scroll_params import ComputerScrollParams as ComputerScrollParams from .process_spawn_response import ProcessSpawnResponse as ProcessSpawnResponse @@ -37,6 +38,7 @@ from .process_resize_response import ProcessResizeResponse as ProcessResizeResponse from .process_status_response import ProcessStatusResponse as ProcessStatusResponse from .browser_telemetry_config import BrowserTelemetryConfig as BrowserTelemetryConfig +from .browser_cdp_connect_event import BrowserCdpConnectEvent as BrowserCdpConnectEvent from .browser_console_log_event import BrowserConsoleLogEvent as BrowserConsoleLogEvent from .computer_press_key_params import ComputerPressKeyParams as ComputerPressKeyParams from .computer_type_text_params import ComputerTypeTextParams as ComputerTypeTextParams @@ -51,14 +53,18 @@ from .browser_console_error_event import BrowserConsoleErrorEvent as BrowserConsoleErrorEvent from .computer_click_mouse_params import ComputerClickMouseParams as ComputerClickMouseParams from .playwright_execute_response import PlaywrightExecuteResponse as PlaywrightExecuteResponse +from .browser_cdp_disconnect_event import BrowserCdpDisconnectEvent as BrowserCdpDisconnectEvent from .browser_interaction_key_event import BrowserInteractionKeyEvent as BrowserInteractionKeyEvent from .browser_network_request_event import BrowserNetworkRequestEvent as BrowserNetworkRequestEvent from .browser_page_navigation_event import BrowserPageNavigationEvent as BrowserPageNavigationEvent from .browser_page_tab_opened_event import BrowserPageTabOpenedEvent as BrowserPageTabOpenedEvent +from .browser_service_crashed_event import BrowserServiceCrashedEvent as BrowserServiceCrashedEvent +from .browser_system_oom_kill_event import BrowserSystemOomKillEvent as BrowserSystemOomKillEvent from .f_set_file_permissions_params import FSetFilePermissionsParams as FSetFilePermissionsParams from .browser_network_response_event import BrowserNetworkResponseEvent as BrowserNetworkResponseEvent from .process_stdout_stream_response import ProcessStdoutStreamResponse as ProcessStdoutStreamResponse from .browser_interaction_click_event import BrowserInteractionClickEvent as BrowserInteractionClickEvent +from .browser_live_view_connect_event import BrowserLiveViewConnectEvent as BrowserLiveViewConnectEvent from .browser_page_layout_shift_event import BrowserPageLayoutShiftEvent as BrowserPageLayoutShiftEvent from .computer_write_clipboard_params import ComputerWriteClipboardParams as ComputerWriteClipboardParams from .browser_monitor_screenshot_event import BrowserMonitorScreenshotEvent as BrowserMonitorScreenshotEvent @@ -67,6 +73,8 @@ from .browser_monitor_reconnected_event import BrowserMonitorReconnectedEvent as BrowserMonitorReconnectedEvent from .browser_page_layout_settled_event import BrowserPageLayoutSettledEvent as BrowserPageLayoutSettledEvent from .browser_telemetry_category_config import BrowserTelemetryCategoryConfig as BrowserTelemetryCategoryConfig +from .browser_captcha_solve_result_event import BrowserCaptchaSolveResultEvent as BrowserCaptchaSolveResultEvent +from .browser_live_view_disconnect_event import BrowserLiveViewDisconnectEvent as BrowserLiveViewDisconnectEvent from .browser_monitor_disconnected_event import BrowserMonitorDisconnectedEvent as BrowserMonitorDisconnectedEvent from .computer_capture_screenshot_params import ComputerCaptureScreenshotParams as ComputerCaptureScreenshotParams from .browser_telemetry_categories_config import BrowserTelemetryCategoriesConfig as BrowserTelemetryCategoriesConfig diff --git a/src/kernel/types/browsers/browser_api_call_event.py b/src/kernel/types/browsers/browser_api_call_event.py new file mode 100644 index 00000000..702596d8 --- /dev/null +++ b/src/kernel/types/browsers/browser_api_call_event.py @@ -0,0 +1,42 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from .browser_event_source import BrowserEventSource + +__all__ = ["BrowserAPICallEvent", "Data"] + + +class Data(BaseModel): + duration_ms: float + """Wall-clock duration of the handler in milliseconds.""" + + operation_id: str + """OpenAPI operationId of the matched route (e.g. processExec, takeScreenshot).""" + + request_id: str + """Per-request identifier from the in-VM API request middleware.""" + + status: int + """HTTP response status code.""" + + +class BrowserAPICallEvent(BaseModel): + """An agent-driven HTTP call handled by the in-VM API server.""" + + category: Literal["control"] + + source: BrowserEventSource + """Provenance metadata identifying which producer emitted the event.""" + + ts: int + """Event timestamp in Unix microseconds.""" + + type: Literal["api_call"] + + data: Optional[Data] = None + + truncated: Optional[bool] = None + """True if the data field was truncated due to size limits.""" diff --git a/src/kernel/types/browsers/browser_captcha_solve_result_event.py b/src/kernel/types/browsers/browser_captcha_solve_result_event.py new file mode 100644 index 00000000..fb8476d4 --- /dev/null +++ b/src/kernel/types/browsers/browser_captcha_solve_result_event.py @@ -0,0 +1,63 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from .browser_event_source import BrowserEventSource + +__all__ = ["BrowserCaptchaSolveResultEvent", "Data"] + + +class Data(BaseModel): + captcha_type: Literal["hcaptcha", "recaptcha_v2", "recaptcha_v3", "turnstile", "geetest", "other"] + """Captcha vendor family. + + Provider-specific task names are normalized into this set; anything not covered + is reported as other. + """ + + duration_ms: float + """Wall-clock duration from solve start to terminal outcome.""" + + status: Literal["success", "failure", "timeout", "abandoned"] + """Terminal outcome. + + success: solver returned a usable solution. failure: solver returned an error + (see error_code). timeout: solver did not return within the caller's wait + budget. abandoned: caller cancelled or the page navigated away mid-solve. + """ + + error_code: Optional[str] = None + """Solver-specific error code on failure (e.g. + + ERROR_CAPTCHA_UNSOLVABLE). Absent on success. + """ + + task_id: Optional[str] = None + """Solver-assigned identifier. Opaque, useful for support cross-references.""" + + website_host: Optional[str] = None + """Host of the page where the captcha was solved.""" + + website_path: Optional[str] = None + """Path of the page where the captcha was solved. Query string excluded.""" + + +class BrowserCaptchaSolveResultEvent(BaseModel): + """A captcha solve attempt reached a terminal outcome.""" + + category: Literal["captcha"] + + source: BrowserEventSource + """Provenance metadata identifying which producer emitted the event.""" + + ts: int + """Event timestamp in Unix microseconds.""" + + type: Literal["captcha_solve_result"] + + data: Optional[Data] = None + + truncated: Optional[bool] = None + """True if the data field was truncated due to size limits.""" diff --git a/src/kernel/types/browsers/browser_cdp_connect_event.py b/src/kernel/types/browsers/browser_cdp_connect_event.py new file mode 100644 index 00000000..ebbc7fa3 --- /dev/null +++ b/src/kernel/types/browsers/browser_cdp_connect_event.py @@ -0,0 +1,29 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from .browser_event_source import BrowserEventSource + +__all__ = ["BrowserCdpConnectEvent"] + + +class BrowserCdpConnectEvent(BaseModel): + """An external client (e.g. + + customer SDK, Playwright, Puppeteer) connected to the CDP WebSocket proxy on this VM. + """ + + category: Literal["connection"] + + source: BrowserEventSource + """Provenance metadata identifying which producer emitted the event.""" + + ts: int + """Event timestamp in Unix microseconds.""" + + type: Literal["cdp_connect"] + + truncated: Optional[bool] = None + """True if the data field was truncated due to size limits.""" diff --git a/src/kernel/types/browsers/browser_cdp_disconnect_event.py b/src/kernel/types/browsers/browser_cdp_disconnect_event.py new file mode 100644 index 00000000..d9260ebc --- /dev/null +++ b/src/kernel/types/browsers/browser_cdp_disconnect_event.py @@ -0,0 +1,49 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from .browser_event_source import BrowserEventSource + +__all__ = ["BrowserCdpDisconnectEvent", "Data"] + + +class Data(BaseModel): + duration_ms: float + """Wall-clock duration of the connection in milliseconds.""" + + message_count: int + """Number of CDP messages relayed across the connection in either direction.""" + + reason: Literal["client_close", "upstream_changed", "upstream_error", "context_cancelled"] + """Why the connection ended. + + client_close: the client initiated the close. upstream_changed: Chromium + restarted mid-session and the proxy tore down so the client could reconnect + against the new upstream. upstream_error: upstream dial or message pump errored. + context_cancelled: the request context was cancelled (typically server + shutdown). + """ + + +class BrowserCdpDisconnectEvent(BaseModel): + """An external client disconnected from the CDP WebSocket proxy on this VM. + + Pair with the immediately preceding cdp_connect on the same stream. + """ + + category: Literal["connection"] + + source: BrowserEventSource + """Provenance metadata identifying which producer emitted the event.""" + + ts: int + """Event timestamp in Unix microseconds.""" + + type: Literal["cdp_disconnect"] + + data: Optional[Data] = None + + truncated: Optional[bool] = None + """True if the data field was truncated due to size limits.""" diff --git a/src/kernel/types/browsers/browser_live_view_connect_event.py b/src/kernel/types/browsers/browser_live_view_connect_event.py new file mode 100644 index 00000000..620e3d36 --- /dev/null +++ b/src/kernel/types/browsers/browser_live_view_connect_event.py @@ -0,0 +1,40 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from .browser_event_source import BrowserEventSource + +__all__ = ["BrowserLiveViewConnectEvent", "Data"] + + +class Data(BaseModel): + session_id: str + """Live view session identifier. + + Stable across reconnects, so a transient network blip can emit two events with + the same session_id. + """ + + +class BrowserLiveViewConnectEvent(BaseModel): + """A live view client connected to the headful browser's WebRTC server. + + Headful only; not emitted for headless images. + """ + + category: Literal["connection"] + + source: BrowserEventSource + """Provenance metadata identifying which producer emitted the event.""" + + ts: int + """Event timestamp in Unix microseconds.""" + + type: Literal["live_view_connect"] + + data: Optional[Data] = None + + truncated: Optional[bool] = None + """True if the data field was truncated due to size limits.""" diff --git a/src/kernel/types/browsers/browser_live_view_disconnect_event.py b/src/kernel/types/browsers/browser_live_view_disconnect_event.py new file mode 100644 index 00000000..a847d34c --- /dev/null +++ b/src/kernel/types/browsers/browser_live_view_disconnect_event.py @@ -0,0 +1,41 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from .browser_event_source import BrowserEventSource + +__all__ = ["BrowserLiveViewDisconnectEvent", "Data"] + + +class Data(BaseModel): + duration_ms: float + """Wall-clock duration of the connection in milliseconds.""" + + session_id: str + """ + Live view session identifier; matches the corresponding live_view_connect event. + """ + + +class BrowserLiveViewDisconnectEvent(BaseModel): + """A live view client disconnected from the headful browser's WebRTC server. + + Pair with live_view_connect by session_id. + """ + + category: Literal["connection"] + + source: BrowserEventSource + """Provenance metadata identifying which producer emitted the event.""" + + ts: int + """Event timestamp in Unix microseconds.""" + + type: Literal["live_view_disconnect"] + + data: Optional[Data] = None + + truncated: Optional[bool] = None + """True if the data field was truncated due to size limits.""" diff --git a/src/kernel/types/browsers/browser_monitor_disconnected_event.py b/src/kernel/types/browsers/browser_monitor_disconnected_event.py index ac37ec6a..2ea3afeb 100644 --- a/src/kernel/types/browsers/browser_monitor_disconnected_event.py +++ b/src/kernel/types/browsers/browser_monitor_disconnected_event.py @@ -20,7 +20,7 @@ class BrowserMonitorDisconnectedEvent(BaseModel): Telemetry events may be dropped until monitor_reconnected arrives. Treat any in-progress computed state (network_idle, page_layout_settled) as unreliable until then. """ - category: Literal["system"] + category: Literal["monitor"] source: BrowserEventSource """Provenance metadata identifying which producer emitted the event.""" diff --git a/src/kernel/types/browsers/browser_monitor_init_failed_event.py b/src/kernel/types/browsers/browser_monitor_init_failed_event.py index dba0e5da..c3a7cfdb 100644 --- a/src/kernel/types/browsers/browser_monitor_init_failed_event.py +++ b/src/kernel/types/browsers/browser_monitor_init_failed_event.py @@ -17,7 +17,7 @@ class Data(BaseModel): class BrowserMonitorInitFailedEvent(BaseModel): """The CDP session could not be initialized.""" - category: Literal["system"] + category: Literal["monitor"] source: BrowserEventSource """Provenance metadata identifying which producer emitted the event.""" diff --git a/src/kernel/types/browsers/browser_monitor_reconnect_failed_event.py b/src/kernel/types/browsers/browser_monitor_reconnect_failed_event.py index 57eec69b..c71bf530 100644 --- a/src/kernel/types/browsers/browser_monitor_reconnect_failed_event.py +++ b/src/kernel/types/browsers/browser_monitor_reconnect_failed_event.py @@ -23,7 +23,7 @@ class BrowserMonitorReconnectFailedEvent(BaseModel): The CDP connection to Chrome could not be re-established after exhausting all reconnection attempts. No further telemetry events will arrive on this session. """ - category: Literal["system"] + category: Literal["monitor"] source: BrowserEventSource """Provenance metadata identifying which producer emitted the event.""" diff --git a/src/kernel/types/browsers/browser_monitor_reconnected_event.py b/src/kernel/types/browsers/browser_monitor_reconnected_event.py index 39363367..c27340c6 100644 --- a/src/kernel/types/browsers/browser_monitor_reconnected_event.py +++ b/src/kernel/types/browsers/browser_monitor_reconnected_event.py @@ -19,7 +19,7 @@ class BrowserMonitorReconnectedEvent(BaseModel): The CDP connection to Chrome was successfully re-established after a disconnection. Events emitted during the gap are lost. Computed state is reset, so navigation and network tracking restart fresh from this point. """ - category: Literal["system"] + category: Literal["monitor"] source: BrowserEventSource """Provenance metadata identifying which producer emitted the event.""" diff --git a/src/kernel/types/browsers/browser_monitor_screenshot_event.py b/src/kernel/types/browsers/browser_monitor_screenshot_event.py index bd2dc65b..02135b47 100644 --- a/src/kernel/types/browsers/browser_monitor_screenshot_event.py +++ b/src/kernel/types/browsers/browser_monitor_screenshot_event.py @@ -17,7 +17,7 @@ class Data(BaseModel): class BrowserMonitorScreenshotEvent(BaseModel): """A periodic screenshot of the browser viewport.""" - category: Literal["system"] + category: Literal["screenshot"] source: BrowserEventSource """Provenance metadata identifying which producer emitted the event.""" diff --git a/src/kernel/types/browsers/browser_service_crashed_event.py b/src/kernel/types/browsers/browser_service_crashed_event.py new file mode 100644 index 00000000..7bc6f1cd --- /dev/null +++ b/src/kernel/types/browsers/browser_service_crashed_event.py @@ -0,0 +1,50 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from .browser_event_source import BrowserEventSource + +__all__ = ["BrowserServiceCrashedEvent", "Data"] + + +class Data(BaseModel): + phase: Literal["startup", "running", "gave_up"] + """Lifecycle phase the crash occurred in. + + startup: the process died before reaching a healthy running state. running: a + previously healthy process died unexpectedly. gave_up: the process manager + exhausted its restart attempts and stopped trying. + """ + + service_name: str + """Program name of the crashed service (e.g. chromium, mutter, kernel-images-api).""" + + pid: Optional[int] = None + """PID of the crashed process. + + Absent when the process manager gave up after exhausting restart attempts. + """ + + +class BrowserServiceCrashedEvent(BaseModel): + """A managed service exited unexpectedly. + + Intentional stops do not produce this event; only unexpected exits and terminal restart-give-up transitions do. + """ + + category: Literal["system"] + + source: BrowserEventSource + """Provenance metadata identifying which producer emitted the event.""" + + ts: int + """Event timestamp in Unix microseconds.""" + + type: Literal["service_crashed"] + + data: Optional[Data] = None + + truncated: Optional[bool] = None + """True if the data field was truncated due to size limits.""" diff --git a/src/kernel/types/browsers/browser_system_oom_kill_event.py b/src/kernel/types/browsers/browser_system_oom_kill_event.py new file mode 100644 index 00000000..2cafb817 --- /dev/null +++ b/src/kernel/types/browsers/browser_system_oom_kill_event.py @@ -0,0 +1,99 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from .browser_event_source import BrowserEventSource + +__all__ = ["BrowserSystemOomKillEvent", "Data", "DataTopTask"] + + +class DataTopTask(BaseModel): + name: str + """Comm of the process (max 15 chars, truncated by the kernel).""" + + pid: int + """PID of the process.""" + + rss_kb: int + """Resident set size in KiB at the moment of the kill.""" + + +class Data(BaseModel): + pid: int + """PID of the killed process.""" + + process_name: str + """ + Comm of the killed process as reported by the kernel (max 15 chars, truncated by + the kernel). + """ + + rss_kb: int + """ + Resident set size of the killed process in KiB (sum of anon-rss, file-rss, and + shmem-rss). + """ + + constraint: Optional[Literal["none", "memcg", "cpuset", "memory_policy"]] = None + """Why the kernel decided to OOM-kill. + + none means global memory exhaustion; memcg means a cgroup memory limit was hit; + cpuset / memory_policy are NUMA/policy-driven kills. Absent on kernels older + than 5.0. + """ + + mem_free_kb: Optional[int] = None + """Free system memory in KiB at the time of the kill. + + Assumes a 4 KiB page size. Does not include reclaimable caches. Absent if the + kernel did not emit a parseable Mem-Info section. + """ + + mem_total_kb: Optional[int] = None + """Total system memory in KiB at the time of the kill. + + Assumes a 4 KiB page size. Absent if the kernel did not emit a parseable + Mem-Info section. + """ + + top_tasks: Optional[List[DataTopTask]] = None + """Top processes by resident-set-size at the moment of the kill, sorted descending. + + Empty if the kernel did not emit the Tasks state table. Capped at 5 entries. + """ + + trigger_pid: Optional[int] = None + """PID of the triggering process. + + Absent if the kernel did not emit the standard header line. + """ + + trigger_process_name: Optional[str] = None + """ + Comm of the process whose allocation request caused the kernel to invoke the + OOM-killer. Often the same as process_name but can differ. Max 15 chars. + """ + + +class BrowserSystemOomKillEvent(BaseModel): + """The Linux kernel OOM-killer terminated a process inside the VM. + + Fires for any process killed by the kernel due to memory exhaustion, including Chrome renderer subprocesses that are not supervised. + """ + + category: Literal["system"] + + source: BrowserEventSource + """Provenance metadata identifying which producer emitted the event.""" + + ts: int + """Event timestamp in Unix microseconds.""" + + type: Literal["system_oom_kill"] + + data: Optional[Data] = None + + truncated: Optional[bool] = None + """True if the data field was truncated due to size limits.""" diff --git a/src/kernel/types/browsers/browser_telemetry_categories_config.py b/src/kernel/types/browsers/browser_telemetry_categories_config.py index 82cdb1db..0013fee3 100644 --- a/src/kernel/types/browsers/browser_telemetry_categories_config.py +++ b/src/kernel/types/browsers/browser_telemetry_categories_config.py @@ -9,24 +9,59 @@ class BrowserTelemetryCategoriesConfig(BaseModel): - """Per-category telemetry capture settings.""" + """Per-category telemetry capture settings. + + Selection is opt-in: set a category to enabled=true to capture it; anything omitted is off. The default set (used by enabled=true with no per-category settings) is the lightweight operational signals: control, connection, system, captcha. The CDP categories (console, network, page, interaction) and screenshot are off by default and must be opted into. + """ + + captcha: Optional[BrowserTelemetryCategoryConfig] = None + """Captcha solve attempt outcomes. On by default.""" + + connection: Optional[BrowserTelemetryCategoryConfig] = None + """Client attach/detach lifecycle for the CDP proxy and live view. On by default.""" console: Optional[BrowserTelemetryCategoryConfig] = None - """Console output (log, warn, error) and uncaught exceptions.""" + """Console output (log, warn, error) and uncaught exceptions. + + CDP category; off by default. + """ + + control: Optional[BrowserTelemetryCategoryConfig] = None + """Agent-driven actions against the browser, such as inbound calls to the in-VM + API. + + On by default. + """ interaction: Optional[BrowserTelemetryCategoryConfig] = None - """User interaction events including clicks, keydowns, and scroll-settled events.""" + """User interaction events including clicks, keydowns, and scroll-settled events. + + CDP category; off by default. + """ network: Optional[BrowserTelemetryCategoryConfig] = None """ HTTP request and response metadata including URL, method, status code, and timing. Request post data is forwarded as-is from CDP. Text response bodies are truncated at 8 KB for structured types (JSON, XML, form data) and 4 KB for other - text types. Binary responses (images, fonts, media) are excluded. + text types. Binary responses (images, fonts, media) are excluded. CDP category; + off by default. """ page: Optional[BrowserTelemetryCategoryConfig] = None """ Page lifecycle events including navigation, DOMContentLoaded, load, layout - shifts, and LCP. + shifts, and LCP. CDP category; off by default. + """ + + screenshot: Optional[BrowserTelemetryCategoryConfig] = None + """Periodic base64-encoded viewport screenshots. + + High volume; off by default and must be opted into. + """ + + system: Optional[BrowserTelemetryCategoryConfig] = None + """Browser VM health, such as out-of-memory kills and managed-service crashes. + + On by default. """ diff --git a/src/kernel/types/browsers/browser_telemetry_categories_config_param.py b/src/kernel/types/browsers/browser_telemetry_categories_config_param.py index 75a3a797..a1520d7c 100644 --- a/src/kernel/types/browsers/browser_telemetry_categories_config_param.py +++ b/src/kernel/types/browsers/browser_telemetry_categories_config_param.py @@ -10,24 +10,59 @@ class BrowserTelemetryCategoriesConfigParam(TypedDict, total=False): - """Per-category telemetry capture settings.""" + """Per-category telemetry capture settings. + + Selection is opt-in: set a category to enabled=true to capture it; anything omitted is off. The default set (used by enabled=true with no per-category settings) is the lightweight operational signals: control, connection, system, captcha. The CDP categories (console, network, page, interaction) and screenshot are off by default and must be opted into. + """ + + captcha: BrowserTelemetryCategoryConfigParam + """Captcha solve attempt outcomes. On by default.""" + + connection: BrowserTelemetryCategoryConfigParam + """Client attach/detach lifecycle for the CDP proxy and live view. On by default.""" console: BrowserTelemetryCategoryConfigParam - """Console output (log, warn, error) and uncaught exceptions.""" + """Console output (log, warn, error) and uncaught exceptions. + + CDP category; off by default. + """ + + control: BrowserTelemetryCategoryConfigParam + """Agent-driven actions against the browser, such as inbound calls to the in-VM + API. + + On by default. + """ interaction: BrowserTelemetryCategoryConfigParam - """User interaction events including clicks, keydowns, and scroll-settled events.""" + """User interaction events including clicks, keydowns, and scroll-settled events. + + CDP category; off by default. + """ network: BrowserTelemetryCategoryConfigParam """ HTTP request and response metadata including URL, method, status code, and timing. Request post data is forwarded as-is from CDP. Text response bodies are truncated at 8 KB for structured types (JSON, XML, form data) and 4 KB for other - text types. Binary responses (images, fonts, media) are excluded. + text types. Binary responses (images, fonts, media) are excluded. CDP category; + off by default. """ page: BrowserTelemetryCategoryConfigParam """ Page lifecycle events including navigation, DOMContentLoaded, load, layout - shifts, and LCP. + shifts, and LCP. CDP category; off by default. + """ + + screenshot: BrowserTelemetryCategoryConfigParam + """Periodic base64-encoded viewport screenshots. + + High volume; off by default and must be opted into. + """ + + system: BrowserTelemetryCategoryConfigParam + """Browser VM health, such as out-of-memory kills and managed-service crashes. + + On by default. """ diff --git a/src/kernel/types/browsers/browser_telemetry_category_config.py b/src/kernel/types/browsers/browser_telemetry_category_config.py index 4a2da2cb..f75616c9 100644 --- a/src/kernel/types/browsers/browser_telemetry_category_config.py +++ b/src/kernel/types/browsers/browser_telemetry_category_config.py @@ -11,4 +11,7 @@ class BrowserTelemetryCategoryConfig(BaseModel): """Per-category telemetry configuration.""" enabled: Optional[bool] = None - """Whether this category is captured. Defaults to true if omitted.""" + """Whether this category is captured. + + Selection is opt-in, so an omitted category is not captured. + """ diff --git a/src/kernel/types/browsers/browser_telemetry_category_config_param.py b/src/kernel/types/browsers/browser_telemetry_category_config_param.py index 3824b4c8..3958f7d8 100644 --- a/src/kernel/types/browsers/browser_telemetry_category_config_param.py +++ b/src/kernel/types/browsers/browser_telemetry_category_config_param.py @@ -11,4 +11,7 @@ class BrowserTelemetryCategoryConfigParam(TypedDict, total=False): """Per-category telemetry configuration.""" enabled: bool - """Whether this category is captured. Defaults to true if omitted.""" + """Whether this category is captured. + + Selection is opt-in, so an omitted category is not captured. + """ diff --git a/src/kernel/types/browsers/browser_telemetry_event.py b/src/kernel/types/browsers/browser_telemetry_event.py index c27297bc..864bb681 100644 --- a/src/kernel/types/browsers/browser_telemetry_event.py +++ b/src/kernel/types/browsers/browser_telemetry_event.py @@ -6,20 +6,28 @@ from typing_extensions import Annotated, TypeAlias from ..._utils import PropertyInfo +from .browser_api_call_event import BrowserAPICallEvent from .browser_page_lcp_event import BrowserPageLcpEvent from .browser_page_load_event import BrowserPageLoadEvent +from .browser_cdp_connect_event import BrowserCdpConnectEvent from .browser_network_idle_event import BrowserNetworkIdleEvent +from .browser_cdp_disconnect_event import BrowserCdpDisconnectEvent from .browser_interaction_key_event import BrowserInteractionKeyEvent from .browser_network_request_event import BrowserNetworkRequestEvent from .browser_page_navigation_event import BrowserPageNavigationEvent from .browser_page_tab_opened_event import BrowserPageTabOpenedEvent +from .browser_service_crashed_event import BrowserServiceCrashedEvent +from .browser_system_oom_kill_event import BrowserSystemOomKillEvent from .browser_network_response_event import BrowserNetworkResponseEvent from .browser_interaction_click_event import BrowserInteractionClickEvent +from .browser_live_view_connect_event import BrowserLiveViewConnectEvent from .browser_page_layout_shift_event import BrowserPageLayoutShiftEvent from .browser_monitor_screenshot_event import BrowserMonitorScreenshotEvent from .browser_monitor_init_failed_event import BrowserMonitorInitFailedEvent from .browser_monitor_reconnected_event import BrowserMonitorReconnectedEvent from .browser_page_layout_settled_event import BrowserPageLayoutSettledEvent +from .browser_captcha_solve_result_event import BrowserCaptchaSolveResultEvent +from .browser_live_view_disconnect_event import BrowserLiveViewDisconnectEvent from .browser_monitor_disconnected_event import BrowserMonitorDisconnectedEvent from .browser_network_loading_failed_event import BrowserNetworkLoadingFailedEvent from .browser_page_dom_content_loaded_event import BrowserPageDomContentLoadedEvent @@ -53,6 +61,14 @@ BrowserMonitorReconnectedEvent, BrowserMonitorReconnectFailedEvent, BrowserMonitorInitFailedEvent, + BrowserAPICallEvent, + BrowserCdpConnectEvent, + BrowserCdpDisconnectEvent, + BrowserLiveViewConnectEvent, + BrowserLiveViewDisconnectEvent, + BrowserCaptchaSolveResultEvent, + BrowserSystemOomKillEvent, + BrowserServiceCrashedEvent, ], PropertyInfo(discriminator="type"), ] diff --git a/src/kernel/types/browsers/telemetry_stream_response.py b/src/kernel/types/browsers/telemetry_stream_response.py index fef04b3f..25dd6650 100644 --- a/src/kernel/types/browsers/telemetry_stream_response.py +++ b/src/kernel/types/browsers/telemetry_stream_response.py @@ -16,11 +16,13 @@ class TelemetryStreamResponse(BaseModel): event: "BrowserTelemetryEvent" """Union type representing any browser telemetry event. - Discriminated on `type`. Events with a `monitor_` prefix (monitor_screenshot, - monitor_disconnected, monitor_reconnected, monitor_reconnect_failed, - monitor_init_failed) are always emitted regardless of the category configuration - in BrowserTelemetryConfig. All other event types are controlled by the - per-category enable/disable flags. + Discriminated on `type`. Each event's `category` determines when it is captured. + The CDP collector-health events (monitor_disconnected, monitor_reconnected, + monitor_reconnect_failed, monitor_init_failed) use the `monitor` category, which + is not user-configurable: it flows automatically whenever any CDP category + (console, network, page, interaction) is captured, and is silent otherwise. + monitor_screenshot uses the opt-in `screenshot` category. All other event types + are controlled by their per-category enable/disable flags. """ seq: int diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index 590640c9..5284214a 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -60,10 +60,15 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: }, telemetry={ "browser": { + "captcha": {"enabled": True}, + "connection": {"enabled": True}, "console": {"enabled": True}, + "control": {"enabled": True}, "interaction": {"enabled": True}, "network": {"enabled": True}, "page": {"enabled": True}, + "screenshot": {"enabled": True}, + "system": {"enabled": True}, }, "enabled": True, }, @@ -171,10 +176,15 @@ def test_method_update_with_all_params(self, client: Kernel) -> None: proxy_id="proxy_id", telemetry={ "browser": { + "captcha": {"enabled": True}, + "connection": {"enabled": True}, "console": {"enabled": True}, + "control": {"enabled": True}, "interaction": {"enabled": True}, "network": {"enabled": True}, "page": {"enabled": True}, + "screenshot": {"enabled": True}, + "system": {"enabled": True}, }, "enabled": True, }, @@ -472,10 +482,15 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> }, telemetry={ "browser": { + "captcha": {"enabled": True}, + "connection": {"enabled": True}, "console": {"enabled": True}, + "control": {"enabled": True}, "interaction": {"enabled": True}, "network": {"enabled": True}, "page": {"enabled": True}, + "screenshot": {"enabled": True}, + "system": {"enabled": True}, }, "enabled": True, }, @@ -583,10 +598,15 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> proxy_id="proxy_id", telemetry={ "browser": { + "captcha": {"enabled": True}, + "connection": {"enabled": True}, "console": {"enabled": True}, + "control": {"enabled": True}, "interaction": {"enabled": True}, "network": {"enabled": True}, "page": {"enabled": True}, + "screenshot": {"enabled": True}, + "system": {"enabled": True}, }, "enabled": True, }, From ea7f78446f0b5eaea0800543b121e52c23a08eed Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 16:28:33 +0000 Subject: [PATCH 438/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 3cb6257c..4c56f2a4 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.63.0" + ".": "0.64.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 710e7a06..a312a055 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.63.0" +version = "0.64.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 52d2f619..b55ca852 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.63.0" # x-release-please-version +__version__ = "0.64.0" # x-release-please-version From eaf7d21ed349bd0d24641a0933566b195d1a63e2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 19:03:34 +0000 Subject: [PATCH 439/448] feat(api): allow setting a name and tags on a pool-acquired browser session --- .stats.yml | 4 +-- src/kernel/resources/browser_pools.py | 35 +++++++++++++++++-- .../types/browser_pool_acquire_params.py | 17 +++++++++ tests/api_resources/test_browser_pools.py | 10 ++++++ 4 files changed, 62 insertions(+), 4 deletions(-) diff --git a/.stats.yml b/.stats.yml index d9e97532..cd599894 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 117 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-9d489e3e43edfa64a754d4281241718e01c85d9a82ef3687df12bbd3c4ff5b42.yml -openapi_spec_hash: a953cafb7f40ec8495dbd7df8bab8bad +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-662a9d6352d842f37e06e0197a61fe10850483302650713345d45780b3128343.yml +openapi_spec_hash: e65977d16d95d48c75d02a1133131149 config_hash: bb7acce8576a50dd449b0c8f58ef0f1d diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py index 203e22ad..5099f446 100644 --- a/src/kernel/resources/browser_pools.py +++ b/src/kernel/resources/browser_pools.py @@ -26,6 +26,7 @@ ) from ..pagination import SyncOffsetPagination, AsyncOffsetPagination from .._base_client import AsyncPaginator, make_request_options +from ..types.tags_param import TagsParam from ..types.browser_pool import BrowserPool from ..types.browser_pool_acquire_response import BrowserPoolAcquireResponse from ..types.shared_params.browser_profile import BrowserProfile @@ -417,6 +418,8 @@ def acquire( id_or_name: str, *, acquire_timeout_seconds: int | Omit = omit, + name: str | Omit = omit, + tags: TagsParam | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -436,6 +439,15 @@ def acquire( calculated time it would take to fill the pool at the currently configured fill rate. + name: Optional human-readable name for the acquired browser session, used to find it + later in the dashboard. Must be unique among active sessions within the pool's + project. Applies to this lease only and is cleared when the browser is released + back to the pool. + + tags: Optional user-defined key-value tags for the acquired browser session, used to + find and group sessions later. Applies to this lease only and are cleared when + the browser is released back to the pool. Up to 50 pairs. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -449,7 +461,11 @@ def acquire( return self._post( path_template("/browser_pools/{id_or_name}/acquire", id_or_name=id_or_name), body=maybe_transform( - {"acquire_timeout_seconds": acquire_timeout_seconds}, + { + "acquire_timeout_seconds": acquire_timeout_seconds, + "name": name, + "tags": tags, + }, browser_pool_acquire_params.BrowserPoolAcquireParams, ), options=make_request_options( @@ -923,6 +939,8 @@ async def acquire( id_or_name: str, *, acquire_timeout_seconds: int | Omit = omit, + name: str | Omit = omit, + tags: TagsParam | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -942,6 +960,15 @@ async def acquire( calculated time it would take to fill the pool at the currently configured fill rate. + name: Optional human-readable name for the acquired browser session, used to find it + later in the dashboard. Must be unique among active sessions within the pool's + project. Applies to this lease only and is cleared when the browser is released + back to the pool. + + tags: Optional user-defined key-value tags for the acquired browser session, used to + find and group sessions later. Applies to this lease only and are cleared when + the browser is released back to the pool. Up to 50 pairs. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -955,7 +982,11 @@ async def acquire( return await self._post( path_template("/browser_pools/{id_or_name}/acquire", id_or_name=id_or_name), body=await async_maybe_transform( - {"acquire_timeout_seconds": acquire_timeout_seconds}, + { + "acquire_timeout_seconds": acquire_timeout_seconds, + "name": name, + "tags": tags, + }, browser_pool_acquire_params.BrowserPoolAcquireParams, ), options=make_request_options( diff --git a/src/kernel/types/browser_pool_acquire_params.py b/src/kernel/types/browser_pool_acquire_params.py index d0df921a..022d69bd 100644 --- a/src/kernel/types/browser_pool_acquire_params.py +++ b/src/kernel/types/browser_pool_acquire_params.py @@ -4,6 +4,8 @@ from typing_extensions import TypedDict +from .tags_param import TagsParam + __all__ = ["BrowserPoolAcquireParams"] @@ -14,3 +16,18 @@ class BrowserPoolAcquireParams(TypedDict, total=False): Defaults to the calculated time it would take to fill the pool at the currently configured fill rate. """ + + name: str + """ + Optional human-readable name for the acquired browser session, used to find it + later in the dashboard. Must be unique among active sessions within the pool's + project. Applies to this lease only and is cleared when the browser is released + back to the pool. + """ + + tags: TagsParam + """ + Optional user-defined key-value tags for the acquired browser session, used to + find and group sessions later. Applies to this lease only and are cleared when + the browser is released back to the pool. Up to 50 pairs. + """ diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py index c71e6c27..e6f8b853 100644 --- a/tests/api_resources/test_browser_pools.py +++ b/tests/api_resources/test_browser_pools.py @@ -309,6 +309,11 @@ def test_method_acquire_with_all_params(self, client: Kernel) -> None: browser_pool = client.browser_pools.acquire( id_or_name="id_or_name", acquire_timeout_seconds=0, + name="checkout-flow-1", + tags={ + "team": "backend", + "env": "staging", + }, ) assert_matches_type(BrowserPoolAcquireResponse, browser_pool, path=["response"]) @@ -738,6 +743,11 @@ async def test_method_acquire_with_all_params(self, async_client: AsyncKernel) - browser_pool = await async_client.browser_pools.acquire( id_or_name="id_or_name", acquire_timeout_seconds=0, + name="checkout-flow-1", + tags={ + "team": "backend", + "env": "staging", + }, ) assert_matches_type(BrowserPoolAcquireResponse, browser_pool, path=["response"]) From 03a044ec81cd5069673cdb9cd02418b42f18fdfe Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:24:53 +0000 Subject: [PATCH 440/448] feat(api): support id-or-name lookup on browser session get/patch/delete --- .stats.yml | 6 +-- api.md | 6 +-- src/kernel/resources/browsers/browsers.py | 52 +++++++++++------------ tests/api_resources/test_browsers.py | 52 +++++++++++------------ 4 files changed, 58 insertions(+), 58 deletions(-) diff --git a/.stats.yml b/.stats.yml index cd599894..dcde96de 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 117 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-662a9d6352d842f37e06e0197a61fe10850483302650713345d45780b3128343.yml -openapi_spec_hash: e65977d16d95d48c75d02a1133131149 -config_hash: bb7acce8576a50dd449b0c8f58ef0f1d +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-08c2d6a44f4cdcbfb6803a3043fdc1a3e33911dec4652cb3a870f01bc584421f.yml +openapi_spec_hash: c816491451347eb93b793cddf6a78648 +config_hash: 9e45c27425021d49b5391f5cc980b046 diff --git a/api.md b/api.md index c5019550..2d311077 100644 --- a/api.md +++ b/api.md @@ -95,11 +95,11 @@ from kernel.types import ( Methods: - client.browsers.create(\*\*params) -> BrowserCreateResponse -- client.browsers.retrieve(id, \*\*params) -> BrowserRetrieveResponse -- client.browsers.update(id, \*\*params) -> BrowserUpdateResponse +- client.browsers.retrieve(id_or_name, \*\*params) -> BrowserRetrieveResponse +- client.browsers.update(id_or_name, \*\*params) -> BrowserUpdateResponse - client.browsers.list(\*\*params) -> SyncOffsetPagination[BrowserListResponse] - client.browsers.curl(id, \*\*params) -> BrowserCurlResponse -- client.browsers.delete_by_id(id) -> None +- client.browsers.delete_by_id(id_or_name) -> None - client.browsers.load_extensions(id, \*\*params) -> None ## Telemetry diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index ad570816..ce8c8848 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -285,7 +285,7 @@ def create( def retrieve( self, - id: str, + id_or_name: str, *, include_deleted: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -309,10 +309,10 @@ def retrieve( timeout: Override the client-level default timeout for this request, in seconds """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return self._get( - path_template("/browsers/{id}", id=id), + path_template("/browsers/{id_or_name}", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -327,7 +327,7 @@ def retrieve( def update( self, - id: str, + id_or_name: str, *, disable_default_proxy: bool | Omit = omit, profile: BrowserProfile | Omit = omit, @@ -370,10 +370,10 @@ def update( timeout: Override the client-level default timeout for this request, in seconds """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return self._patch( - path_template("/browsers/{id}", id=id), + path_template("/browsers/{id_or_name}", id_or_name=id_or_name), body=maybe_transform( { "disable_default_proxy": disable_default_proxy, @@ -525,7 +525,7 @@ def curl( def delete_by_id( self, - id: str, + id_or_name: 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. @@ -535,7 +535,7 @@ def delete_by_id( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ - Delete a browser session by ID + Delete a browser session by ID or name Args: extra_headers: Send extra headers @@ -546,11 +546,11 @@ def delete_by_id( timeout: Override the client-level default timeout for this request, in seconds """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( - path_template("/browsers/{id}", id=id), + path_template("/browsers/{id_or_name}", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -792,7 +792,7 @@ async def create( async def retrieve( self, - id: str, + id_or_name: str, *, include_deleted: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -816,10 +816,10 @@ async def retrieve( timeout: Override the client-level default timeout for this request, in seconds """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return await self._get( - path_template("/browsers/{id}", id=id), + path_template("/browsers/{id_or_name}", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -834,7 +834,7 @@ async def retrieve( async def update( self, - id: str, + id_or_name: str, *, disable_default_proxy: bool | Omit = omit, profile: BrowserProfile | Omit = omit, @@ -877,10 +877,10 @@ async def update( timeout: Override the client-level default timeout for this request, in seconds """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return await self._patch( - path_template("/browsers/{id}", id=id), + path_template("/browsers/{id_or_name}", id_or_name=id_or_name), body=await async_maybe_transform( { "disable_default_proxy": disable_default_proxy, @@ -1032,7 +1032,7 @@ async def curl( async def delete_by_id( self, - id: str, + id_or_name: 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. @@ -1042,7 +1042,7 @@ async def delete_by_id( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: """ - Delete a browser session by ID + Delete a browser session by ID or name Args: extra_headers: Send extra headers @@ -1053,11 +1053,11 @@ async def delete_by_id( timeout: Override the client-level default timeout for this request, in seconds """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not id_or_name: + raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( - path_template("/browsers/{id}", id=id), + path_template("/browsers/{id_or_name}", id_or_name=id_or_name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index 5284214a..e09d4705 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -107,7 +107,7 @@ def test_streaming_response_create(self, client: Kernel) -> None: @parametrize def test_method_retrieve(self, client: Kernel) -> None: browser = client.browsers.retrieve( - id="htzv5orfit78e1m2biiifpbv", + id_or_name="htzv5orfit78e1m2biiifpbv", ) assert_matches_type(BrowserRetrieveResponse, browser, path=["response"]) @@ -115,7 +115,7 @@ def test_method_retrieve(self, client: Kernel) -> None: @parametrize def test_method_retrieve_with_all_params(self, client: Kernel) -> None: browser = client.browsers.retrieve( - id="htzv5orfit78e1m2biiifpbv", + id_or_name="htzv5orfit78e1m2biiifpbv", include_deleted=True, ) assert_matches_type(BrowserRetrieveResponse, browser, path=["response"]) @@ -124,7 +124,7 @@ def test_method_retrieve_with_all_params(self, client: Kernel) -> None: @parametrize def test_raw_response_retrieve(self, client: Kernel) -> None: response = client.browsers.with_raw_response.retrieve( - id="htzv5orfit78e1m2biiifpbv", + id_or_name="htzv5orfit78e1m2biiifpbv", ) assert response.is_closed is True @@ -136,7 +136,7 @@ def test_raw_response_retrieve(self, client: Kernel) -> None: @parametrize def test_streaming_response_retrieve(self, client: Kernel) -> None: with client.browsers.with_streaming_response.retrieve( - id="htzv5orfit78e1m2biiifpbv", + id_or_name="htzv5orfit78e1m2biiifpbv", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -149,16 +149,16 @@ def test_streaming_response_retrieve(self, client: Kernel) -> None: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_retrieve(self, client: Kernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): client.browsers.with_raw_response.retrieve( - id="", + id_or_name="", ) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_update(self, client: Kernel) -> None: browser = client.browsers.update( - id="htzv5orfit78e1m2biiifpbv", + id_or_name="htzv5orfit78e1m2biiifpbv", ) assert_matches_type(BrowserUpdateResponse, browser, path=["response"]) @@ -166,7 +166,7 @@ def test_method_update(self, client: Kernel) -> None: @parametrize def test_method_update_with_all_params(self, client: Kernel) -> None: browser = client.browsers.update( - id="htzv5orfit78e1m2biiifpbv", + id_or_name="htzv5orfit78e1m2biiifpbv", disable_default_proxy=True, profile={ "id": "id", @@ -201,7 +201,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None: @parametrize def test_raw_response_update(self, client: Kernel) -> None: response = client.browsers.with_raw_response.update( - id="htzv5orfit78e1m2biiifpbv", + id_or_name="htzv5orfit78e1m2biiifpbv", ) assert response.is_closed is True @@ -213,7 +213,7 @@ def test_raw_response_update(self, client: Kernel) -> None: @parametrize def test_streaming_response_update(self, client: Kernel) -> None: with client.browsers.with_streaming_response.update( - id="htzv5orfit78e1m2biiifpbv", + id_or_name="htzv5orfit78e1m2biiifpbv", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -226,9 +226,9 @@ def test_streaming_response_update(self, client: Kernel) -> None: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_update(self, client: Kernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): client.browsers.with_raw_response.update( - id="", + id_or_name="", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -369,7 +369,7 @@ def test_streaming_response_delete_by_id(self, client: Kernel) -> None: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_delete_by_id(self, client: Kernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): client.browsers.with_raw_response.delete_by_id( "", ) @@ -529,7 +529,7 @@ async def test_streaming_response_create(self, async_client: AsyncKernel) -> Non @parametrize async def test_method_retrieve(self, async_client: AsyncKernel) -> None: browser = await async_client.browsers.retrieve( - id="htzv5orfit78e1m2biiifpbv", + id_or_name="htzv5orfit78e1m2biiifpbv", ) assert_matches_type(BrowserRetrieveResponse, browser, path=["response"]) @@ -537,7 +537,7 @@ async def test_method_retrieve(self, async_client: AsyncKernel) -> None: @parametrize async def test_method_retrieve_with_all_params(self, async_client: AsyncKernel) -> None: browser = await async_client.browsers.retrieve( - id="htzv5orfit78e1m2biiifpbv", + id_or_name="htzv5orfit78e1m2biiifpbv", include_deleted=True, ) assert_matches_type(BrowserRetrieveResponse, browser, path=["response"]) @@ -546,7 +546,7 @@ async def test_method_retrieve_with_all_params(self, async_client: AsyncKernel) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.with_raw_response.retrieve( - id="htzv5orfit78e1m2biiifpbv", + id_or_name="htzv5orfit78e1m2biiifpbv", ) assert response.is_closed is True @@ -558,7 +558,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: async with async_client.browsers.with_streaming_response.retrieve( - id="htzv5orfit78e1m2biiifpbv", + id_or_name="htzv5orfit78e1m2biiifpbv", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -571,16 +571,16 @@ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> N @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): await async_client.browsers.with_raw_response.retrieve( - id="", + id_or_name="", ) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_update(self, async_client: AsyncKernel) -> None: browser = await async_client.browsers.update( - id="htzv5orfit78e1m2biiifpbv", + id_or_name="htzv5orfit78e1m2biiifpbv", ) assert_matches_type(BrowserUpdateResponse, browser, path=["response"]) @@ -588,7 +588,7 @@ async def test_method_update(self, async_client: AsyncKernel) -> None: @parametrize async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> None: browser = await async_client.browsers.update( - id="htzv5orfit78e1m2biiifpbv", + id_or_name="htzv5orfit78e1m2biiifpbv", disable_default_proxy=True, profile={ "id": "id", @@ -623,7 +623,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> @parametrize async def test_raw_response_update(self, async_client: AsyncKernel) -> None: response = await async_client.browsers.with_raw_response.update( - id="htzv5orfit78e1m2biiifpbv", + id_or_name="htzv5orfit78e1m2biiifpbv", ) assert response.is_closed is True @@ -635,7 +635,7 @@ async def test_raw_response_update(self, async_client: AsyncKernel) -> None: @parametrize async def test_streaming_response_update(self, async_client: AsyncKernel) -> None: async with async_client.browsers.with_streaming_response.update( - id="htzv5orfit78e1m2biiifpbv", + id_or_name="htzv5orfit78e1m2biiifpbv", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -648,9 +648,9 @@ async def test_streaming_response_update(self, async_client: AsyncKernel) -> Non @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_update(self, async_client: AsyncKernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): await async_client.browsers.with_raw_response.update( - id="", + id_or_name="", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -791,7 +791,7 @@ async def test_streaming_response_delete_by_id(self, async_client: AsyncKernel) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_delete_by_id(self, async_client: AsyncKernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): await async_client.browsers.with_raw_response.delete_by_id( "", ) From 4ddc40c9d1798344f0ab15f9801b582f7e8d5d35 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 23:29:16 +0000 Subject: [PATCH 441/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 4c56f2a4..57726a4f 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.64.0" + ".": "0.65.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index a312a055..75d7d508 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.64.0" +version = "0.65.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index b55ca852..7a6f830a 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.64.0" # x-release-please-version +__version__ = "0.65.0" # x-release-please-version From a7d5f41d5f22c060759560a9c1c0adb836a3f0d4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 18:59:15 +0000 Subject: [PATCH 442/448] feat: Add org-level default per-project concurrency cap --- .stats.yml | 8 +- api.md | 15 ++ src/kernel/_client.py | 38 +++ src/kernel/resources/__init__.py | 14 + src/kernel/resources/organization/__init__.py | 33 +++ src/kernel/resources/organization/limits.py | 245 ++++++++++++++++++ .../resources/organization/organization.py | 108 ++++++++ src/kernel/types/organization/__init__.py | 6 + .../types/organization/limit_update_params.py | 17 ++ src/kernel/types/organization/org_limits.py | 24 ++ tests/api_resources/organization/__init__.py | 1 + .../api_resources/organization/test_limits.py | 152 +++++++++++ 12 files changed, 657 insertions(+), 4 deletions(-) create mode 100644 src/kernel/resources/organization/__init__.py create mode 100644 src/kernel/resources/organization/limits.py create mode 100644 src/kernel/resources/organization/organization.py create mode 100644 src/kernel/types/organization/__init__.py create mode 100644 src/kernel/types/organization/limit_update_params.py create mode 100644 src/kernel/types/organization/org_limits.py create mode 100644 tests/api_resources/organization/__init__.py create mode 100644 tests/api_resources/organization/test_limits.py diff --git a/.stats.yml b/.stats.yml index dcde96de..fc334e13 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 117 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-08c2d6a44f4cdcbfb6803a3043fdc1a3e33911dec4652cb3a870f01bc584421f.yml -openapi_spec_hash: c816491451347eb93b793cddf6a78648 -config_hash: 9e45c27425021d49b5391f5cc980b046 +configured_endpoints: 119 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-76f6c461ca9fd01958f3315a4f5d558ef80896c8aa0496e38caef55d4bd51dbd.yml +openapi_spec_hash: bb8124b6016b73022c52e6ef5b7220a4 +config_hash: 80eef1b592110714ea55cd26c470fabb diff --git a/api.md b/api.md index 2d311077..4b24f102 100644 --- a/api.md +++ b/api.md @@ -423,6 +423,21 @@ Methods: - client.projects.limits.retrieve(id) -> ProjectLimits - client.projects.limits.update(id, \*\*params) -> ProjectLimits +# Organization + +## Limits + +Types: + +```python +from kernel.types.organization import OrgLimits, UpdateOrgLimitsRequest +``` + +Methods: + +- client.organization.limits.retrieve() -> OrgLimits +- client.organization.limits.update(\*\*params) -> OrgLimits + # APIKeys Types: diff --git a/src/kernel/_client.py b/src/kernel/_client.py index 7e627454..07f1a8af 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -47,6 +47,7 @@ credentials, deployments, invocations, + organization, browser_pools, credential_providers, ) @@ -63,6 +64,7 @@ from .resources.browsers.browsers import BrowsersResource, AsyncBrowsersResource from .resources.projects.projects import ProjectsResource, AsyncProjectsResource from .resources.credential_providers import CredentialProvidersResource, AsyncCredentialProvidersResource + from .resources.organization.organization import OrganizationResource, AsyncOrganizationResource __all__ = [ "ENVIRONMENTS", @@ -246,6 +248,12 @@ def projects(self) -> ProjectsResource: return ProjectsResource(self) + @cached_property + def organization(self) -> OrganizationResource: + from .resources.organization import OrganizationResource + + return OrganizationResource(self) + @cached_property def api_keys(self) -> APIKeysResource: """Create and manage API keys for organization and project-scoped access.""" @@ -539,6 +547,12 @@ def projects(self) -> AsyncProjectsResource: return AsyncProjectsResource(self) + @cached_property + def organization(self) -> AsyncOrganizationResource: + from .resources.organization import AsyncOrganizationResource + + return AsyncOrganizationResource(self) + @cached_property def api_keys(self) -> AsyncAPIKeysResource: """Create and manage API keys for organization and project-scoped access.""" @@ -750,6 +764,12 @@ def projects(self) -> projects.ProjectsResourceWithRawResponse: return ProjectsResourceWithRawResponse(self._client.projects) + @cached_property + def organization(self) -> organization.OrganizationResourceWithRawResponse: + from .resources.organization import OrganizationResourceWithRawResponse + + return OrganizationResourceWithRawResponse(self._client.organization) + @cached_property def api_keys(self) -> api_keys.APIKeysResourceWithRawResponse: """Create and manage API keys for organization and project-scoped access.""" @@ -847,6 +867,12 @@ def projects(self) -> projects.AsyncProjectsResourceWithRawResponse: return AsyncProjectsResourceWithRawResponse(self._client.projects) + @cached_property + def organization(self) -> organization.AsyncOrganizationResourceWithRawResponse: + from .resources.organization import AsyncOrganizationResourceWithRawResponse + + return AsyncOrganizationResourceWithRawResponse(self._client.organization) + @cached_property def api_keys(self) -> api_keys.AsyncAPIKeysResourceWithRawResponse: """Create and manage API keys for organization and project-scoped access.""" @@ -944,6 +970,12 @@ def projects(self) -> projects.ProjectsResourceWithStreamingResponse: return ProjectsResourceWithStreamingResponse(self._client.projects) + @cached_property + def organization(self) -> organization.OrganizationResourceWithStreamingResponse: + from .resources.organization import OrganizationResourceWithStreamingResponse + + return OrganizationResourceWithStreamingResponse(self._client.organization) + @cached_property def api_keys(self) -> api_keys.APIKeysResourceWithStreamingResponse: """Create and manage API keys for organization and project-scoped access.""" @@ -1041,6 +1073,12 @@ def projects(self) -> projects.AsyncProjectsResourceWithStreamingResponse: return AsyncProjectsResourceWithStreamingResponse(self._client.projects) + @cached_property + def organization(self) -> organization.AsyncOrganizationResourceWithStreamingResponse: + from .resources.organization import AsyncOrganizationResourceWithStreamingResponse + + return AsyncOrganizationResourceWithStreamingResponse(self._client.organization) + @cached_property def api_keys(self) -> api_keys.AsyncAPIKeysResourceWithStreamingResponse: """Create and manage API keys for organization and project-scoped access.""" diff --git a/src/kernel/resources/__init__.py b/src/kernel/resources/__init__.py index 64b98338..1497ad67 100644 --- a/src/kernel/resources/__init__.py +++ b/src/kernel/resources/__init__.py @@ -88,6 +88,14 @@ InvocationsResourceWithStreamingResponse, AsyncInvocationsResourceWithStreamingResponse, ) +from .organization import ( + OrganizationResource, + AsyncOrganizationResource, + OrganizationResourceWithRawResponse, + AsyncOrganizationResourceWithRawResponse, + OrganizationResourceWithStreamingResponse, + AsyncOrganizationResourceWithStreamingResponse, +) from .browser_pools import ( BrowserPoolsResource, AsyncBrowserPoolsResource, @@ -172,6 +180,12 @@ "AsyncProjectsResourceWithRawResponse", "ProjectsResourceWithStreamingResponse", "AsyncProjectsResourceWithStreamingResponse", + "OrganizationResource", + "AsyncOrganizationResource", + "OrganizationResourceWithRawResponse", + "AsyncOrganizationResourceWithRawResponse", + "OrganizationResourceWithStreamingResponse", + "AsyncOrganizationResourceWithStreamingResponse", "APIKeysResource", "AsyncAPIKeysResource", "APIKeysResourceWithRawResponse", diff --git a/src/kernel/resources/organization/__init__.py b/src/kernel/resources/organization/__init__.py new file mode 100644 index 00000000..68c28d60 --- /dev/null +++ b/src/kernel/resources/organization/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .limits import ( + LimitsResource, + AsyncLimitsResource, + LimitsResourceWithRawResponse, + AsyncLimitsResourceWithRawResponse, + LimitsResourceWithStreamingResponse, + AsyncLimitsResourceWithStreamingResponse, +) +from .organization import ( + OrganizationResource, + AsyncOrganizationResource, + OrganizationResourceWithRawResponse, + AsyncOrganizationResourceWithRawResponse, + OrganizationResourceWithStreamingResponse, + AsyncOrganizationResourceWithStreamingResponse, +) + +__all__ = [ + "LimitsResource", + "AsyncLimitsResource", + "LimitsResourceWithRawResponse", + "AsyncLimitsResourceWithRawResponse", + "LimitsResourceWithStreamingResponse", + "AsyncLimitsResourceWithStreamingResponse", + "OrganizationResource", + "AsyncOrganizationResource", + "OrganizationResourceWithRawResponse", + "AsyncOrganizationResourceWithRawResponse", + "OrganizationResourceWithStreamingResponse", + "AsyncOrganizationResourceWithStreamingResponse", +] diff --git a/src/kernel/resources/organization/limits.py b/src/kernel/resources/organization/limits.py new file mode 100644 index 00000000..4bf68845 --- /dev/null +++ b/src/kernel/resources/organization/limits.py @@ -0,0 +1,245 @@ +# 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 Body, Omit, Query, Headers, NotGiven, omit, not_given +from ..._utils import maybe_transform, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..._base_client import make_request_options +from ...types.organization import limit_update_params +from ...types.organization.org_limits import OrgLimits + +__all__ = ["LimitsResource", "AsyncLimitsResource"] + + +class LimitsResource(SyncAPIResource): + """Read and manage organization-level limits.""" + + @cached_property + def with_raw_response(self) -> LimitsResourceWithRawResponse: + """ + 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/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return LimitsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> LimitsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response + """ + return LimitsResourceWithStreamingResponse(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, + ) -> OrgLimits: + """ + Get the organization's concurrent session ceiling and the default per-project + concurrency cap applied to projects without an explicit override. + """ + return self._get( + "/org/limits", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=OrgLimits, + ) + + def update( + self, + *, + default_project_max_concurrent_sessions: Optional[int] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> OrgLimits: + """ + Set the default per-project concurrency cap applied to projects without an + explicit override. Set the value to 0 to remove the default; omit to leave it + unchanged. The default cannot exceed the organization's concurrent session + ceiling. + + Args: + default_project_max_concurrent_sessions: Default maximum concurrent browser sessions for projects without an explicit + override. Set to 0 to remove the default; omit to leave unchanged. Cannot exceed + the organization's concurrent session ceiling. + + 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._patch( + "/org/limits", + body=maybe_transform( + {"default_project_max_concurrent_sessions": default_project_max_concurrent_sessions}, + limit_update_params.LimitUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=OrgLimits, + ) + + +class AsyncLimitsResource(AsyncAPIResource): + """Read and manage organization-level limits.""" + + @cached_property + def with_raw_response(self) -> AsyncLimitsResourceWithRawResponse: + """ + 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/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncLimitsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncLimitsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response + """ + return AsyncLimitsResourceWithStreamingResponse(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, + ) -> OrgLimits: + """ + Get the organization's concurrent session ceiling and the default per-project + concurrency cap applied to projects without an explicit override. + """ + return await self._get( + "/org/limits", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=OrgLimits, + ) + + async def update( + self, + *, + default_project_max_concurrent_sessions: Optional[int] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> OrgLimits: + """ + Set the default per-project concurrency cap applied to projects without an + explicit override. Set the value to 0 to remove the default; omit to leave it + unchanged. The default cannot exceed the organization's concurrent session + ceiling. + + Args: + default_project_max_concurrent_sessions: Default maximum concurrent browser sessions for projects without an explicit + override. Set to 0 to remove the default; omit to leave unchanged. Cannot exceed + the organization's concurrent session ceiling. + + 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._patch( + "/org/limits", + body=await async_maybe_transform( + {"default_project_max_concurrent_sessions": default_project_max_concurrent_sessions}, + limit_update_params.LimitUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=OrgLimits, + ) + + +class LimitsResourceWithRawResponse: + def __init__(self, limits: LimitsResource) -> None: + self._limits = limits + + self.retrieve = to_raw_response_wrapper( + limits.retrieve, + ) + self.update = to_raw_response_wrapper( + limits.update, + ) + + +class AsyncLimitsResourceWithRawResponse: + def __init__(self, limits: AsyncLimitsResource) -> None: + self._limits = limits + + self.retrieve = async_to_raw_response_wrapper( + limits.retrieve, + ) + self.update = async_to_raw_response_wrapper( + limits.update, + ) + + +class LimitsResourceWithStreamingResponse: + def __init__(self, limits: LimitsResource) -> None: + self._limits = limits + + self.retrieve = to_streamed_response_wrapper( + limits.retrieve, + ) + self.update = to_streamed_response_wrapper( + limits.update, + ) + + +class AsyncLimitsResourceWithStreamingResponse: + def __init__(self, limits: AsyncLimitsResource) -> None: + self._limits = limits + + self.retrieve = async_to_streamed_response_wrapper( + limits.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + limits.update, + ) diff --git a/src/kernel/resources/organization/organization.py b/src/kernel/resources/organization/organization.py new file mode 100644 index 00000000..21163122 --- /dev/null +++ b/src/kernel/resources/organization/organization.py @@ -0,0 +1,108 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .limits import ( + LimitsResource, + AsyncLimitsResource, + LimitsResourceWithRawResponse, + AsyncLimitsResourceWithRawResponse, + LimitsResourceWithStreamingResponse, + AsyncLimitsResourceWithStreamingResponse, +) +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource + +__all__ = ["OrganizationResource", "AsyncOrganizationResource"] + + +class OrganizationResource(SyncAPIResource): + @cached_property + def limits(self) -> LimitsResource: + """Read and manage organization-level limits.""" + return LimitsResource(self._client) + + @cached_property + def with_raw_response(self) -> OrganizationResourceWithRawResponse: + """ + 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/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return OrganizationResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> OrganizationResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response + """ + return OrganizationResourceWithStreamingResponse(self) + + +class AsyncOrganizationResource(AsyncAPIResource): + @cached_property + def limits(self) -> AsyncLimitsResource: + """Read and manage organization-level limits.""" + return AsyncLimitsResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncOrganizationResourceWithRawResponse: + """ + 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/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncOrganizationResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncOrganizationResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response + """ + return AsyncOrganizationResourceWithStreamingResponse(self) + + +class OrganizationResourceWithRawResponse: + def __init__(self, organization: OrganizationResource) -> None: + self._organization = organization + + @cached_property + def limits(self) -> LimitsResourceWithRawResponse: + """Read and manage organization-level limits.""" + return LimitsResourceWithRawResponse(self._organization.limits) + + +class AsyncOrganizationResourceWithRawResponse: + def __init__(self, organization: AsyncOrganizationResource) -> None: + self._organization = organization + + @cached_property + def limits(self) -> AsyncLimitsResourceWithRawResponse: + """Read and manage organization-level limits.""" + return AsyncLimitsResourceWithRawResponse(self._organization.limits) + + +class OrganizationResourceWithStreamingResponse: + def __init__(self, organization: OrganizationResource) -> None: + self._organization = organization + + @cached_property + def limits(self) -> LimitsResourceWithStreamingResponse: + """Read and manage organization-level limits.""" + return LimitsResourceWithStreamingResponse(self._organization.limits) + + +class AsyncOrganizationResourceWithStreamingResponse: + def __init__(self, organization: AsyncOrganizationResource) -> None: + self._organization = organization + + @cached_property + def limits(self) -> AsyncLimitsResourceWithStreamingResponse: + """Read and manage organization-level limits.""" + return AsyncLimitsResourceWithStreamingResponse(self._organization.limits) diff --git a/src/kernel/types/organization/__init__.py b/src/kernel/types/organization/__init__.py new file mode 100644 index 00000000..d2411f9a --- /dev/null +++ b/src/kernel/types/organization/__init__.py @@ -0,0 +1,6 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .org_limits import OrgLimits as OrgLimits +from .limit_update_params import LimitUpdateParams as LimitUpdateParams diff --git a/src/kernel/types/organization/limit_update_params.py b/src/kernel/types/organization/limit_update_params.py new file mode 100644 index 00000000..619eae91 --- /dev/null +++ b/src/kernel/types/organization/limit_update_params.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import TypedDict + +__all__ = ["LimitUpdateParams"] + + +class LimitUpdateParams(TypedDict, total=False): + default_project_max_concurrent_sessions: Optional[int] + """ + Default maximum concurrent browser sessions for projects without an explicit + override. Set to 0 to remove the default; omit to leave unchanged. Cannot exceed + the organization's concurrent session ceiling. + """ diff --git a/src/kernel/types/organization/org_limits.py b/src/kernel/types/organization/org_limits.py new file mode 100644 index 00000000..da9153c1 --- /dev/null +++ b/src/kernel/types/organization/org_limits.py @@ -0,0 +1,24 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = ["OrgLimits"] + + +class OrgLimits(BaseModel): + default_project_max_concurrent_sessions: Optional[int] = None + """ + Default maximum concurrent browser sessions applied to every project that has no + explicit per-project override. Null means no org-level default, so such projects + are uncapped (only the org-wide limit applies). Applies to existing and newly + created projects. + """ + + max_concurrent_sessions: Optional[int] = None + """ + The organization's effective concurrent browser session ceiling, from its plan + or an override. Read-only and shared across all projects in the org; a + per-project default cannot exceed it. + """ diff --git a/tests/api_resources/organization/__init__.py b/tests/api_resources/organization/__init__.py new file mode 100644 index 00000000..fd8019a9 --- /dev/null +++ b/tests/api_resources/organization/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/organization/test_limits.py b/tests/api_resources/organization/test_limits.py new file mode 100644 index 00000000..a9dca6d0 --- /dev/null +++ b/tests/api_resources/organization/test_limits.py @@ -0,0 +1,152 @@ +# 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 kernel import Kernel, AsyncKernel +from tests.utils import assert_matches_type +from kernel.types.organization import OrgLimits + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestLimits: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve(self, client: Kernel) -> None: + limit = client.organization.limits.retrieve() + assert_matches_type(OrgLimits, limit, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: Kernel) -> None: + response = client.organization.limits.with_raw_response.retrieve() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + limit = response.parse() + assert_matches_type(OrgLimits, limit, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: Kernel) -> None: + with client.organization.limits.with_streaming_response.retrieve() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + limit = response.parse() + assert_matches_type(OrgLimits, limit, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update(self, client: Kernel) -> None: + limit = client.organization.limits.update() + assert_matches_type(OrgLimits, limit, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update_with_all_params(self, client: Kernel) -> None: + limit = client.organization.limits.update( + default_project_max_concurrent_sessions=0, + ) + assert_matches_type(OrgLimits, limit, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_update(self, client: Kernel) -> None: + response = client.organization.limits.with_raw_response.update() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + limit = response.parse() + assert_matches_type(OrgLimits, limit, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_update(self, client: Kernel) -> None: + with client.organization.limits.with_streaming_response.update() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + limit = response.parse() + assert_matches_type(OrgLimits, limit, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncLimits: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve(self, async_client: AsyncKernel) -> None: + limit = await async_client.organization.limits.retrieve() + assert_matches_type(OrgLimits, limit, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: + response = await async_client.organization.limits.with_raw_response.retrieve() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + limit = await response.parse() + assert_matches_type(OrgLimits, limit, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: + async with async_client.organization.limits.with_streaming_response.retrieve() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + limit = await response.parse() + assert_matches_type(OrgLimits, limit, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update(self, async_client: AsyncKernel) -> None: + limit = await async_client.organization.limits.update() + assert_matches_type(OrgLimits, limit, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> None: + limit = await async_client.organization.limits.update( + default_project_max_concurrent_sessions=0, + ) + assert_matches_type(OrgLimits, limit, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_update(self, async_client: AsyncKernel) -> None: + response = await async_client.organization.limits.with_raw_response.update() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + limit = await response.parse() + assert_matches_type(OrgLimits, limit, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_update(self, async_client: AsyncKernel) -> None: + async with async_client.organization.limits.with_streaming_response.update() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + limit = await response.parse() + assert_matches_type(OrgLimits, limit, path=["response"]) + + assert cast(Any, response.is_closed) is True From b945e05c1667d0fbf8c28084b9939939bbc60852 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:31:38 +0000 Subject: [PATCH 443/448] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index fc334e13..17293bfb 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 119 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-76f6c461ca9fd01958f3315a4f5d558ef80896c8aa0496e38caef55d4bd51dbd.yml -openapi_spec_hash: bb8124b6016b73022c52e6ef5b7220a4 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-f9a96fe14f0b3c93230a26f9b64827a35a19a28d4e7cd2719315c4d76cce78fc.yml +openapi_spec_hash: 852e2a64b850f759ccbcf81b1579497a config_hash: 80eef1b592110714ea55cd26c470fabb From bbc8e5ac31f97f5d964deecba5d0960215fb834e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 20:41:45 +0000 Subject: [PATCH 444/448] feat: Support updating browser session name and tags via PATCH --- .stats.yml | 4 +-- src/kernel/resources/browsers/browsers.py | 38 +++++++++++++++++++---- src/kernel/types/browser_create_params.py | 7 +++-- src/kernel/types/browser_update_params.py | 16 ++++++++++ tests/api_resources/test_browsers.py | 10 ++++++ 5 files changed, 64 insertions(+), 11 deletions(-) diff --git a/.stats.yml b/.stats.yml index 17293bfb..5ad69241 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 119 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-f9a96fe14f0b3c93230a26f9b64827a35a19a28d4e7cd2719315c4d76cce78fc.yml -openapi_spec_hash: 852e2a64b850f759ccbcf81b1579497a +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e66c3f8aedccc39104386a3ec619f3fdcef7e8b00d9e5aa82e414a1b387351c2.yml +openapi_spec_hash: afeddf18ebc3da1521b3e6f6739411fa config_hash: 80eef1b592110714ea55cd26c470fabb diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index ce8c8848..11fec37c 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -202,8 +202,8 @@ def create( view. name: Optional human-readable name for the browser session, used to find it later in - the dashboard. Must be unique among active sessions within the project. Set at - creation time only. + the dashboard. Must be unique among active sessions within the project. Can be + changed later via PATCH /browsers/{id_or_name}. profile: Profile selection for the browser session. Provide either id or name. If specified, the matching profile will be loaded into the browser session. @@ -220,7 +220,8 @@ def create( mechanisms. tags: Optional user-defined key-value tags for the browser session, used to find and - group sessions later. Set at creation time only. Up to 50 pairs. + group sessions later. Can be changed later via PATCH /browsers/{id_or_name}. Up + to 50 pairs. telemetry: Telemetry configuration for the browser session. Set enabled to true to start capture using VM defaults, or provide browser category settings. If omitted, @@ -330,8 +331,10 @@ def update( id_or_name: str, *, disable_default_proxy: bool | Omit = omit, + name: Optional[str] | Omit = omit, profile: BrowserProfile | Omit = omit, proxy_id: Optional[str] | Omit = omit, + tags: Optional[TagsParam] | Omit = omit, telemetry: Optional[browser_update_params.Telemetry] | Omit = omit, viewport: browser_update_params.Viewport | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -348,12 +351,20 @@ def update( disable_default_proxy: If true, stealth browsers connect directly instead of using the default stealth proxy. + name: Human-readable name for the browser session. Omit to leave unchanged, set to an + empty string to clear the name. When set, must be unique among active sessions + within the project. + profile: Profile to load into the browser session. Only allowed if the session does not already have a profile loaded. proxy_id: ID of the proxy to use. Omit to leave unchanged, set to empty string to remove proxy. + tags: User-defined key-value tags for the browser session. Omit to leave unchanged. + Provide a map to replace the entire tag set (full replace, not a merge). Set to + an empty object ({}) to clear all tags. Up to 50 pairs. + telemetry: Telemetry configuration. Omit, set to null, or set to an empty object ({}) to leave the existing configuration unchanged. Set enabled to true to enable capture using VM defaults. Set enabled to false to stop capture. Provide browser @@ -377,8 +388,10 @@ def update( body=maybe_transform( { "disable_default_proxy": disable_default_proxy, + "name": name, "profile": profile, "proxy_id": proxy_id, + "tags": tags, "telemetry": telemetry, "viewport": viewport, }, @@ -709,8 +722,8 @@ async def create( view. name: Optional human-readable name for the browser session, used to find it later in - the dashboard. Must be unique among active sessions within the project. Set at - creation time only. + the dashboard. Must be unique among active sessions within the project. Can be + changed later via PATCH /browsers/{id_or_name}. profile: Profile selection for the browser session. Provide either id or name. If specified, the matching profile will be loaded into the browser session. @@ -727,7 +740,8 @@ async def create( mechanisms. tags: Optional user-defined key-value tags for the browser session, used to find and - group sessions later. Set at creation time only. Up to 50 pairs. + group sessions later. Can be changed later via PATCH /browsers/{id_or_name}. Up + to 50 pairs. telemetry: Telemetry configuration for the browser session. Set enabled to true to start capture using VM defaults, or provide browser category settings. If omitted, @@ -837,8 +851,10 @@ async def update( id_or_name: str, *, disable_default_proxy: bool | Omit = omit, + name: Optional[str] | Omit = omit, profile: BrowserProfile | Omit = omit, proxy_id: Optional[str] | Omit = omit, + tags: Optional[TagsParam] | Omit = omit, telemetry: Optional[browser_update_params.Telemetry] | Omit = omit, viewport: browser_update_params.Viewport | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -855,12 +871,20 @@ async def update( disable_default_proxy: If true, stealth browsers connect directly instead of using the default stealth proxy. + name: Human-readable name for the browser session. Omit to leave unchanged, set to an + empty string to clear the name. When set, must be unique among active sessions + within the project. + profile: Profile to load into the browser session. Only allowed if the session does not already have a profile loaded. proxy_id: ID of the proxy to use. Omit to leave unchanged, set to empty string to remove proxy. + tags: User-defined key-value tags for the browser session. Omit to leave unchanged. + Provide a map to replace the entire tag set (full replace, not a merge). Set to + an empty object ({}) to clear all tags. Up to 50 pairs. + telemetry: Telemetry configuration. Omit, set to null, or set to an empty object ({}) to leave the existing configuration unchanged. Set enabled to true to enable capture using VM defaults. Set enabled to false to stop capture. Provide browser @@ -884,8 +908,10 @@ async def update( body=await async_maybe_transform( { "disable_default_proxy": disable_default_proxy, + "name": name, "profile": profile, "proxy_id": proxy_id, + "tags": tags, "telemetry": telemetry, "viewport": viewport, }, diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index 6d7b80b3..01308f38 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -53,8 +53,8 @@ class BrowserCreateParams(TypedDict, total=False): name: str """ Optional human-readable name for the browser session, used to find it later in - the dashboard. Must be unique among active sessions within the project. Set at - creation time only. + the dashboard. Must be unique among active sessions within the project. Can be + changed later via PATCH /browsers/{id_or_name}. """ profile: BrowserProfile @@ -86,7 +86,8 @@ class BrowserCreateParams(TypedDict, total=False): tags: TagsParam """ Optional user-defined key-value tags for the browser session, used to find and - group sessions later. Set at creation time only. Up to 50 pairs. + group sessions later. Can be changed later via PATCH /browsers/{id_or_name}. Up + to 50 pairs. """ telemetry: Optional[Telemetry] diff --git a/src/kernel/types/browser_update_params.py b/src/kernel/types/browser_update_params.py index 2626917c..7488be4f 100644 --- a/src/kernel/types/browser_update_params.py +++ b/src/kernel/types/browser_update_params.py @@ -5,6 +5,7 @@ from typing import Optional from typing_extensions import TypedDict +from .tags_param import TagsParam from .shared_params.browser_profile import BrowserProfile from .shared_params.browser_viewport import BrowserViewport from .browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam @@ -19,6 +20,13 @@ class BrowserUpdateParams(TypedDict, total=False): proxy. """ + name: Optional[str] + """Human-readable name for the browser session. + + Omit to leave unchanged, set to an empty string to clear the name. When set, + must be unique among active sessions within the project. + """ + profile: BrowserProfile """Profile to load into the browser session. @@ -31,6 +39,14 @@ class BrowserUpdateParams(TypedDict, total=False): Omit to leave unchanged, set to empty string to remove proxy. """ + tags: Optional[TagsParam] + """User-defined key-value tags for the browser session. + + Omit to leave unchanged. Provide a map to replace the entire tag set (full + replace, not a merge). Set to an empty object ({}) to clear all tags. Up to 50 + pairs. + """ + telemetry: Optional[Telemetry] """Telemetry configuration. diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index e09d4705..322fffed 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -168,12 +168,17 @@ def test_method_update_with_all_params(self, client: Kernel) -> None: browser = client.browsers.update( id_or_name="htzv5orfit78e1m2biiifpbv", disable_default_proxy=True, + name="checkout-flow-1", profile={ "id": "id", "name": "name", "save_changes": True, }, proxy_id="proxy_id", + tags={ + "team": "backend", + "env": "staging", + }, telemetry={ "browser": { "captcha": {"enabled": True}, @@ -590,12 +595,17 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> browser = await async_client.browsers.update( id_or_name="htzv5orfit78e1m2biiifpbv", disable_default_proxy=True, + name="checkout-flow-1", profile={ "id": "id", "name": "name", "save_changes": True, }, proxy_id="proxy_id", + tags={ + "team": "backend", + "env": "staging", + }, telemetry={ "browser": { "captcha": {"enabled": True}, From 1ccb79aa5255770498152b3c0e4173e9c43da455 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 21:14:41 +0000 Subject: [PATCH 445/448] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 57726a4f..b429f966 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.65.0" + ".": "0.66.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 75d7d508..c9fef2a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.65.0" +version = "0.66.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 7a6f830a..eda0bdfb 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.65.0" # x-release-please-version +__version__ = "0.66.0" # x-release-please-version From b7694014c8cea71d1810ce3f3a40b20174642feb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 19:08:20 +0000 Subject: [PATCH 446/448] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 5ad69241..f6e3d871 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 119 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e66c3f8aedccc39104386a3ec619f3fdcef7e8b00d9e5aa82e414a1b387351c2.yml -openapi_spec_hash: afeddf18ebc3da1521b3e6f6739411fa +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-663f08b43eda6383b99ab0f3b1cd049d62f22c81daf149d8e84ff18f53c84c89.yml +openapi_spec_hash: 29ea250251cc14d70e3f8f737ebc1466 config_hash: 80eef1b592110714ea55cd26c470fabb From c7b39df2387d27173b560e3a5a553548a94e2d6e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 19:42:18 +0000 Subject: [PATCH 447/448] docs(api): correct project-scoping descriptions in OpenAPI spec --- .stats.yml | 4 ++-- src/kernel/resources/auth/connections.py | 24 +++++++++---------- src/kernel/resources/browser_pools.py | 20 ++++++++-------- src/kernel/resources/browsers/browsers.py | 8 +++---- src/kernel/resources/credentials.py | 10 ++++---- src/kernel/resources/extensions.py | 4 ++-- src/kernel/resources/proxies.py | 12 +++++----- .../types/auth/connection_create_params.py | 5 ++-- .../types/auth/connection_login_params.py | 5 ++-- .../types/auth/connection_update_params.py | 5 ++-- src/kernel/types/browser_create_params.py | 2 +- src/kernel/types/browser_pool.py | 2 +- .../types/browser_pool_create_params.py | 2 +- .../types/browser_pool_update_params.py | 2 +- 14 files changed, 53 insertions(+), 52 deletions(-) diff --git a/.stats.yml b/.stats.yml index f6e3d871..1d17b2b3 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 119 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-663f08b43eda6383b99ab0f3b1cd049d62f22c81daf149d8e84ff18f53c84c89.yml -openapi_spec_hash: 29ea250251cc14d70e3f8f737ebc1466 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-c6458d9c6cb5d7e3d8309c79b69eba3a22269e0ecc0bdafbaee00fde4b302e99.yml +openapi_spec_hash: ee77b293c4bda91c1a32cfdd12b8739e config_hash: 80eef1b592110714ea55cd26c470fabb diff --git a/src/kernel/resources/auth/connections.py b/src/kernel/resources/auth/connections.py index dc912aba..362e6129 100644 --- a/src/kernel/resources/auth/connections.py +++ b/src/kernel/resources/auth/connections.py @@ -136,8 +136,8 @@ def create( login_url: Optional login page URL to skip discovery - proxy: Proxy selection. Provide either id or name. The proxy must belong to the - caller's org. + proxy: Proxy selection. Provide either id or name. The proxy must be in the same + project as the resource referencing it. record_session: Whether to record browser sessions for this connection by default. Useful for debugging. Can be overridden per-login. Defaults to false. @@ -264,8 +264,8 @@ def update( login_url: Login page URL. Set to empty string to clear. - proxy: Proxy selection. Provide either id or name. The proxy must belong to the - caller's org. + proxy: Proxy selection. Provide either id or name. The proxy must be in the same + project as the resource referencing it. record_session: Whether to record browser sessions for this connection by default @@ -457,8 +457,8 @@ def login( credentials are stored. Args: - proxy: Proxy selection. Provide either id or name. The proxy must belong to the - caller's org. + proxy: Proxy selection. Provide either id or name. The proxy must be in the same + project as the resource referencing it. record_session: Override the connection's default for recording this login's browser session. When omitted, the connection's record_session default is used. @@ -653,8 +653,8 @@ async def create( login_url: Optional login page URL to skip discovery - proxy: Proxy selection. Provide either id or name. The proxy must belong to the - caller's org. + proxy: Proxy selection. Provide either id or name. The proxy must be in the same + project as the resource referencing it. record_session: Whether to record browser sessions for this connection by default. Useful for debugging. Can be overridden per-login. Defaults to false. @@ -781,8 +781,8 @@ async def update( login_url: Login page URL. Set to empty string to clear. - proxy: Proxy selection. Provide either id or name. The proxy must belong to the - caller's org. + proxy: Proxy selection. Provide either id or name. The proxy must be in the same + project as the resource referencing it. record_session: Whether to record browser sessions for this connection by default @@ -974,8 +974,8 @@ async def login( credentials are stored. Args: - proxy: Proxy selection. Provide either id or name. The proxy must belong to the - caller's org. + proxy: Proxy selection. Provide either id or name. The proxy must be in the same + project as the resource referencing it. record_session: Override the connection's default for recording this login's browser session. When omitted, the connection's record_session default is used. diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py index 5099f446..fc32c951 100644 --- a/src/kernel/resources/browser_pools.py +++ b/src/kernel/resources/browser_pools.py @@ -109,8 +109,8 @@ def create( specified, the matching profile will be loaded into the browser session. Profiles must be created beforehand. - proxy_id: Optional proxy to associate to the browser session. Must reference a proxy - belonging to the caller's org. + proxy_id: Optional proxy to associate to the browser session. Must reference a proxy in + the same project as the browser session. start_url: Optional URL to navigate to when a new browser is warmed into the pool. Best-effort: failures to navigate do not fail pool fill. Only applied to @@ -256,8 +256,8 @@ def update( specified, the matching profile will be loaded into the browser session. Profiles must be created beforehand. - proxy_id: Optional proxy to associate to the browser session. Must reference a proxy - belonging to the caller's org. + proxy_id: Optional proxy to associate to the browser session. Must reference a proxy in + the same project as the browser session. size: Number of browsers to maintain in the pool. The maximum size is determined by your organization's pooled sessions limit (the sum of all pool sizes cannot @@ -338,7 +338,7 @@ def list( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SyncOffsetPagination[BrowserPool]: """ - List browser pools owned by the caller's organization. + List browser pools in the resolved project. Args: limit: Limit the number of browser pools to return. @@ -630,8 +630,8 @@ async def create( specified, the matching profile will be loaded into the browser session. Profiles must be created beforehand. - proxy_id: Optional proxy to associate to the browser session. Must reference a proxy - belonging to the caller's org. + proxy_id: Optional proxy to associate to the browser session. Must reference a proxy in + the same project as the browser session. start_url: Optional URL to navigate to when a new browser is warmed into the pool. Best-effort: failures to navigate do not fail pool fill. Only applied to @@ -777,8 +777,8 @@ async def update( specified, the matching profile will be loaded into the browser session. Profiles must be created beforehand. - proxy_id: Optional proxy to associate to the browser session. Must reference a proxy - belonging to the caller's org. + proxy_id: Optional proxy to associate to the browser session. Must reference a proxy in + the same project as the browser session. size: Number of browsers to maintain in the pool. The maximum size is determined by your organization's pooled sessions limit (the sum of all pool sizes cannot @@ -859,7 +859,7 @@ def list( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncPaginator[BrowserPool, AsyncOffsetPagination[BrowserPool]]: """ - List browser pools owned by the caller's organization. + List browser pools in the resolved project. Args: limit: Limit the number of browser pools to return. diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 11fec37c..666a6dc0 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -209,8 +209,8 @@ def create( specified, the matching profile will be loaded into the browser session. Profiles must be created beforehand. - proxy_id: Optional proxy to associate to the browser session. Must reference a proxy - belonging to the caller's org. + proxy_id: Optional proxy to associate to the browser session. Must reference a proxy in + the same project as the browser session. start_url: Optional URL to open when the browser session is created. Navigation is best-effort, so navigation failures do not prevent the session from being @@ -729,8 +729,8 @@ async def create( specified, the matching profile will be loaded into the browser session. Profiles must be created beforehand. - proxy_id: Optional proxy to associate to the browser session. Must reference a proxy - belonging to the caller's org. + proxy_id: Optional proxy to associate to the browser session. Must reference a proxy in + the same project as the browser session. start_url: Optional URL to open when the browser session is created. Navigation is best-effort, so navigation failures do not prevent the session from being diff --git a/src/kernel/resources/credentials.py b/src/kernel/resources/credentials.py index 7d00faab..4c90ef85 100644 --- a/src/kernel/resources/credentials.py +++ b/src/kernel/resources/credentials.py @@ -212,10 +212,9 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SyncOffsetPagination[Credential]: - """List credentials owned by the caller's organization. + """List credentials in the resolved project. - Credential values are not - returned. + Credential values are not returned. Args: domain: Filter by domain @@ -509,10 +508,9 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncPaginator[Credential, AsyncOffsetPagination[Credential]]: - """List credentials owned by the caller's organization. + """List credentials in the resolved project. - Credential values are not - returned. + Credential values are not returned. Args: domain: Filter by domain diff --git a/src/kernel/resources/extensions.py b/src/kernel/resources/extensions.py index 5b7215be..8d7acb14 100644 --- a/src/kernel/resources/extensions.py +++ b/src/kernel/resources/extensions.py @@ -70,7 +70,7 @@ def list( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SyncOffsetPagination[ExtensionListResponse]: """ - List extensions owned by the caller's organization. + List extensions in the resolved project. Args: limit: Limit the number of extensions to return. @@ -308,7 +308,7 @@ def list( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncPaginator[ExtensionListResponse, AsyncOffsetPagination[ExtensionListResponse]]: """ - List extensions owned by the caller's organization. + List extensions in the resolved project. Args: limit: Limit the number of extensions to return. diff --git a/src/kernel/resources/proxies.py b/src/kernel/resources/proxies.py index 2239a854..831e7819 100644 --- a/src/kernel/resources/proxies.py +++ b/src/kernel/resources/proxies.py @@ -65,7 +65,7 @@ def create( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProxyCreateResponse: """ - Create a new proxy configuration for the caller's organization. + Create a new proxy configuration in the resolved project. Args: type: Proxy type to use. In terms of quality for avoiding bot-detection, from best to @@ -117,7 +117,7 @@ def retrieve( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProxyRetrieveResponse: """ - Retrieve a proxy belonging to the caller's organization by ID. + Retrieve a proxy in the resolved project by ID. Args: extra_headers: Send extra headers @@ -151,7 +151,7 @@ def list( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SyncOffsetPagination[ProxyListResponse]: """ - List proxies owned by the caller's organization. + List proxies in the resolved project. Args: limit: Limit the number of proxies to return. @@ -313,7 +313,7 @@ async def create( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProxyCreateResponse: """ - Create a new proxy configuration for the caller's organization. + Create a new proxy configuration in the resolved project. Args: type: Proxy type to use. In terms of quality for avoiding bot-detection, from best to @@ -365,7 +365,7 @@ async def retrieve( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProxyRetrieveResponse: """ - Retrieve a proxy belonging to the caller's organization by ID. + Retrieve a proxy in the resolved project by ID. Args: extra_headers: Send extra headers @@ -399,7 +399,7 @@ def list( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncPaginator[ProxyListResponse, AsyncOffsetPagination[ProxyListResponse]]: """ - List proxies owned by the caller's organization. + List proxies in the resolved project. Args: limit: Limit the number of proxies to return. diff --git a/src/kernel/types/auth/connection_create_params.py b/src/kernel/types/auth/connection_create_params.py index acde944c..e5a969ce 100644 --- a/src/kernel/types/auth/connection_create_params.py +++ b/src/kernel/types/auth/connection_create_params.py @@ -83,7 +83,8 @@ class ConnectionCreateParams(TypedDict, total=False): proxy: Proxy """Proxy selection. - Provide either id or name. The proxy must belong to the caller's org. + Provide either id or name. The proxy must be in the same project as the resource + referencing it. """ record_session: bool @@ -124,7 +125,7 @@ class Credential(TypedDict, total=False): class Proxy(TypedDict, total=False): """Proxy selection. - Provide either id or name. The proxy must belong to the caller's org. + Provide either id or name. The proxy must be in the same project as the resource referencing it. """ id: str diff --git a/src/kernel/types/auth/connection_login_params.py b/src/kernel/types/auth/connection_login_params.py index 39756cb1..5e136cc3 100644 --- a/src/kernel/types/auth/connection_login_params.py +++ b/src/kernel/types/auth/connection_login_params.py @@ -11,7 +11,8 @@ class ConnectionLoginParams(TypedDict, total=False): proxy: Proxy """Proxy selection. - Provide either id or name. The proxy must belong to the caller's org. + Provide either id or name. The proxy must be in the same project as the resource + referencing it. """ record_session: bool @@ -24,7 +25,7 @@ class ConnectionLoginParams(TypedDict, total=False): class Proxy(TypedDict, total=False): """Proxy selection. - Provide either id or name. The proxy must belong to the caller's org. + Provide either id or name. The proxy must be in the same project as the resource referencing it. """ id: str diff --git a/src/kernel/types/auth/connection_update_params.py b/src/kernel/types/auth/connection_update_params.py index 8875f609..e0dadf70 100644 --- a/src/kernel/types/auth/connection_update_params.py +++ b/src/kernel/types/auth/connection_update_params.py @@ -50,7 +50,8 @@ class ConnectionUpdateParams(TypedDict, total=False): proxy: Proxy """Proxy selection. - Provide either id or name. The proxy must belong to the caller's org. + Provide either id or name. The proxy must be in the same project as the resource + referencing it. """ record_session: bool @@ -85,7 +86,7 @@ class Credential(TypedDict, total=False): class Proxy(TypedDict, total=False): """Proxy selection. - Provide either id or name. The proxy must belong to the caller's org. + Provide either id or name. The proxy must be in the same project as the resource referencing it. """ id: str diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index 01308f38..10d53936 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -67,7 +67,7 @@ class BrowserCreateParams(TypedDict, total=False): proxy_id: str """Optional proxy to associate to the browser session. - Must reference a proxy belonging to the caller's org. + Must reference a proxy in the same project as the browser session. """ start_url: str diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py index 8ead6807..8ad50acf 100644 --- a/src/kernel/types/browser_pool.py +++ b/src/kernel/types/browser_pool.py @@ -60,7 +60,7 @@ class BrowserPoolConfig(BaseModel): proxy_id: Optional[str] = None """Optional proxy to associate to the browser session. - Must reference a proxy belonging to the caller's org. + Must reference a proxy in the same project as the browser session. """ start_url: Optional[str] = None diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py index 1ffeeb75..33d04ce4 100644 --- a/src/kernel/types/browser_pool_create_params.py +++ b/src/kernel/types/browser_pool_create_params.py @@ -59,7 +59,7 @@ class BrowserPoolCreateParams(TypedDict, total=False): proxy_id: str """Optional proxy to associate to the browser session. - Must reference a proxy belonging to the caller's org. + Must reference a proxy in the same project as the browser session. """ start_url: str diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py index 07501695..e80b5aa0 100644 --- a/src/kernel/types/browser_pool_update_params.py +++ b/src/kernel/types/browser_pool_update_params.py @@ -58,7 +58,7 @@ class BrowserPoolUpdateParams(TypedDict, total=False): proxy_id: str """Optional proxy to associate to the browser session. - Must reference a proxy belonging to the caller's org. + Must reference a proxy in the same project as the browser session. """ size: int From 07e47364a8c87764ba81dbf51b9e5f856d0078a0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 21:01:00 +0000 Subject: [PATCH 448/448] feat: Add project_id SDK client option mapped to X-Kernel-Project-Id --- .stats.yml | 4 ++-- src/kernel/_client.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 1d17b2b3..a1640098 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 119 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-c6458d9c6cb5d7e3d8309c79b69eba3a22269e0ecc0bdafbaee00fde4b302e99.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-51549f813f3002e18c6ca8d850cc0c7932828d511c151e0412c73b6798d19e30.yml openapi_spec_hash: ee77b293c4bda91c1a32cfdd12b8739e -config_hash: 80eef1b592110714ea55cd26c470fabb +config_hash: 57567e00b41af47cef1b78e51b747aa0 diff --git a/src/kernel/_client.py b/src/kernel/_client.py index 07f1a8af..1e1cb732 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -87,6 +87,7 @@ class Kernel(SyncAPIClient): # client options api_key: str + project_id: str | None _environment: Literal["production", "development"] | NotGiven @@ -94,6 +95,7 @@ def __init__( self, *, api_key: str | None = None, + project_id: str | None = None, environment: Literal["production", "development"] | NotGiven = not_given, base_url: str | httpx.URL | None | NotGiven = not_given, timeout: float | Timeout | None | NotGiven = not_given, @@ -126,6 +128,8 @@ def __init__( ) self.api_key = api_key + self.project_id = project_id + self._environment = environment base_url_env = os.environ.get("KERNEL_BASE_URL") @@ -293,6 +297,7 @@ def default_headers(self) -> dict[str, str | Omit]: return { **super().default_headers, "X-Stainless-Async": "false", + "X-Kernel-Project-Id": self.project_id if self.project_id is not None else Omit(), **self._custom_headers, } @@ -300,6 +305,7 @@ def copy( self, *, api_key: str | None = None, + project_id: str | None = None, environment: Literal["production", "development"] | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = not_given, @@ -335,6 +341,7 @@ def copy( http_client = http_client or self._client return self.__class__( api_key=api_key or self.api_key, + project_id=project_id or self.project_id, base_url=base_url or self.base_url, environment=environment or self._environment, timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, @@ -386,6 +393,7 @@ def _make_status_error( class AsyncKernel(AsyncAPIClient): # client options api_key: str + project_id: str | None _environment: Literal["production", "development"] | NotGiven @@ -393,6 +401,7 @@ def __init__( self, *, api_key: str | None = None, + project_id: str | None = None, environment: Literal["production", "development"] | NotGiven = not_given, base_url: str | httpx.URL | None | NotGiven = not_given, timeout: float | Timeout | None | NotGiven = not_given, @@ -425,6 +434,8 @@ def __init__( ) self.api_key = api_key + self.project_id = project_id + self._environment = environment base_url_env = os.environ.get("KERNEL_BASE_URL") @@ -592,6 +603,7 @@ def default_headers(self) -> dict[str, str | Omit]: return { **super().default_headers, "X-Stainless-Async": f"async:{get_async_library()}", + "X-Kernel-Project-Id": self.project_id if self.project_id is not None else Omit(), **self._custom_headers, } @@ -599,6 +611,7 @@ def copy( self, *, api_key: str | None = None, + project_id: str | None = None, environment: Literal["production", "development"] | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = not_given, @@ -634,6 +647,7 @@ def copy( http_client = http_client or self._client return self.__class__( api_key=api_key or self.api_key, + project_id=project_id or self.project_id, base_url=base_url or self.base_url, environment=environment or self._environment, timeout=self.timeout if isinstance(timeout, NotGiven) else timeout,