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/251] 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 0000000..ff261ba --- /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 0000000..c17fdc1 --- /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 0000000..0d9000d --- /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 0000000..8779740 --- /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 0000000..43077b2 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.9.18 diff --git a/.stats.yml b/.stats.yml new file mode 100644 index 0000000..7bdb6b3 --- /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 0000000..492ca37 --- /dev/null +++ b/Brewfile @@ -0,0 +1,2 @@ +brew "rye" + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..24d5b0a --- /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 0000000..b32a077 --- /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 6866e5b..06527eb 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 0000000..bd2ba47 --- /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 0000000..ec9b481 --- /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 0000000..826054e --- /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 0000000..d8c73e9 --- /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 0000000..0745431 --- /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 0000000..53bca7f --- /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 0000000..b3465a4 --- /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 0000000..efd90ea --- /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 0000000..4071919 --- /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 0000000..e84fe62 --- /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 0000000..667ec2d --- /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 0000000..b5b8891 --- /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 0000000..d2814ae --- /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 0000000..2b87845 --- /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 0000000..0cf2bd2 --- /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 0000000..1093761 --- /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 0000000..34308dd --- /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 0000000..aa9f227 --- /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 0000000..92d9ee6 --- /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 0000000..6ddf2c7 --- /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 0000000..53cd14c --- /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 0000000..df2a05e --- /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 0000000..798956f --- /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 0000000..274320c --- /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 0000000..eb51ab5 --- /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 0000000..89c72c3 --- /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 0000000..e3131a3 --- /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 0000000..2b0c5c3 --- /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 0000000..d4fda26 --- /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 0000000..4eff94b --- /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 0000000..0f239a3 --- /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 0000000..89aa712 --- /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 0000000..f4a0208 --- /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 0000000..ad7ec71 --- /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 0000000..b0cc20a --- /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 0000000..1bac954 --- /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 0000000..ea3cf3f --- /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 0000000..3d085d7 --- /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 0000000..5e2c99f --- /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 0000000..e69de29 diff --git a/src/kernel/resources/__init__.py b/src/kernel/resources/__init__.py new file mode 100644 index 0000000..a0d1ea6 --- /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 0000000..74c900c --- /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 0000000..ae79d38 --- /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 0000000..9577c2f --- /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 0000000..78a11f2 --- /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 0000000..6a214df --- /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 0000000..773fd45 --- /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 0000000..801ec5c --- /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 0000000..8b3de1f --- /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 0000000..d4e46da --- /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 0000000..fd8019a --- /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 0000000..fd8019a --- /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 0000000..7f086b5 --- /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 0000000..1aa4a1c --- /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 0000000..6d3cc20 --- /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 0000000..af5626b --- /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 0000000..f989403 --- /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 0000000..83b72cd --- /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 0000000..e5cf4a1 --- /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 0000000..62b874f --- /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 0000000..4f41217 --- /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 0000000..78ae641 --- /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 0000000..7186db8 --- /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 0000000..bf62a9b --- /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 0000000..4b8e4e4 --- /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 0000000..a418f4f --- /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 0000000..8c9c8ae --- /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 0000000..3b18d48 --- /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 0000000..d81c8f4 --- /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/251] 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 0000000..120241d --- /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 0000000..5e7787d --- /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 0000000..c476280 --- /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 7bdb6b3..8a75f87 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 24d5b0a..c486484 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 06527eb..edbe158 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 0000000..47a8dca --- /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 b3465a4..f5fc711 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 0000000..942ec08 --- /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 df2a05e..63dab8a 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 3d085d7..9c761c5 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 74c900c..30c666c 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 ae79d38..3e3da66 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/251] 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 8a75f87..e041923 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 edbe158..b7d6ce2 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 f5fc711..7e62a6c 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 efd90ea..6af49a0 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 4071919..e46d87a 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/251] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index e041923..b59ce7c 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/251] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index b59ce7c..d93970e 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/251] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index d93970e..49a57d8 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/251] codegen metadata --- .stats.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 49a57d8..b66a1bc 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/251] 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 b66a1bc..25b168b 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 b7d6ce2..cbec5e4 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 7e62a6c..f5fc711 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 6af49a0..efd90ea 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 e46d87a..4071919 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/251] 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 c476280..ba6c348 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 f5fc711..c3d2b1e 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 9c761c5..eb6a1d3 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/251] 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 25b168b..f654ed7 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 30c666c..997a27f 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 773fd45..414da98 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 801ec5c..e76a9fd 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 7f086b5..26d0ef1 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/251] 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 ba6c348..f14b480 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 c3d2b1e..c4a4017 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 eb6a1d3..ed7a6fd 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/251] 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 1093761..2a6614e 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 0000000..006a639 --- /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/251] 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 f14b480..aaf968a 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 c4a4017..3c2101a 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 ed7a6fd..5f681ed 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/251] 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 f654ed7..3b0a5f2 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 cbec5e4..0e3afec 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/251] 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 aaf968a..b56c3d0 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 3c2101a..8764ccd 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 5f681ed..f5e8430 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/251] 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 3b0a5f2..1a7891f 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 0e3afec..ab4e765 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 2a6614e..4c0f254 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 aa9f227..28871ba 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 f989403..0efa1c2 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/251] 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 b56c3d0..e8285b7 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 8764ccd..1bca88c 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 f5e8430..55b53c6 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/251] 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 1a7891f..42510a9 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 ec9b481..fe6fb48 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 997a27f..24cc154 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 9577c2f..2403d11 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 8b3de1f..0000000 --- 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 26d0ef1..962d7aa 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/251] 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 e8285b7..4f9005e 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 1bca88c..a714f44 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 55b53c6..2451c60 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/251] 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 42510a9..6af3fb9 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 fe6fb48..ec9b481 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 24cc154..997a27f 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 2403d11..9577c2f 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 0000000..8b3de1f --- /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 962d7aa..26d0ef1 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/251] 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 4f9005e..b5db7ce 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 a714f44..710383c 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 2451c60..76a7255 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/251] 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 6af3fb9..cbc2ff8 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 ab4e765..1cbfb1f 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 ec9b481..581c76f 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 997a27f..4514880 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 3e3da66..3edf8c0 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 9577c2f..32a4768 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 78a11f2..ff7242c 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 6a214df..e82164e 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 0000000..73389be --- /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 26d0ef1..08efe17 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 1aa4a1c..3280e05 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 0efa1c2..360b622 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/251] 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 b5db7ce..c373724 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 710383c..37fb003 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 76a7255..924d714 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/251] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index cbc2ff8..1b796fe 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/251] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 1b796fe..fbd0261 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/251] 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 fbd0261..d6d797a 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 1cbfb1f..801b34f 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 4514880..023f214 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 ff7242c..790743d 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 08efe17..8719486 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 360b622..713ce3c 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/251] 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 c373724..46b9b6b 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 37fb003..5045418 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 924d714..7716ecb 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/251] 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 0d9000d..51b16df 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 0000000..42692db --- /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/251] 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 42692db..7b344b4 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/251] 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 7b344b4..c55ebbc 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/251] 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 d6d797a..6f21bcb 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 801b34f..1f5a5bb 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 581c76f..3456b56 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 28871ba..bf6fbb4 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 a0d1ea6..647bde6 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 023f214..0000000 --- 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 0000000..5602ad7 --- /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 0000000..848b765 --- /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 0000000..0b88fd1 --- /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 0000000..4401501 --- /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 3edf8c0..0000000 --- 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 0000000..2aa307a --- /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 32a4768..282c889 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 e82164e..0000000 --- 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 414da98..0000000 --- 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 e76a9fd..0000000 --- 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 8b3de1f..0000000 --- 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 0000000..f4d451c --- /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 790743d..92ff258 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 0000000..f801195 --- /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 0000000..a97a2c5 --- /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 0000000..df4a166 --- /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 0000000..f328b14 --- /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 0000000..0944a61 --- /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 d4e46da..647dfc8 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 73389be..0000000 --- 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 0000000..e84bb01 --- /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 0000000..fd8019a --- /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 0000000..e2f2a3d --- /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 0000000..61af031 --- /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 8719486..0000000 --- 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 3280e05..0000000 --- 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 0000000..91fc83e --- /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 713ce3c..00d7a01 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/251] 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 46b9b6b..3b005e5 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 5045418..b8cbab8 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 7716ecb..edb86b3 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/251] 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 6f21bcb..24b7a04 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 0b88fd1..89f7992 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 92ff258..cd1a7b5 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 e2f2a3d..3b4ea03 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/251] 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 24b7a04..4dfbf42 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 1f5a5bb..b9a0ae8 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/251] 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 3b005e5..ee49ac2 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 b8cbab8..7f1d0e7 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 edb86b3..ec24ea7 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/251] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 4dfbf42..6d03c39 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/251] 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 6d03c39..01f342c 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 3456b56..32a9bb0 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 89f7992..9a1ceca 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 f4d451c..425fffd 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 0000000..4374485 --- /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 3b4ea03..0a93c44 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/251] 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 01f342c..01c41ad 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 32a9bb0..63d7b00 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 848b765..9a5f667 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 282c889..e7c3cec 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 0000000..d4506a3 --- /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 0000000..8a6f621 --- /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 0000000..b902576 --- /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/251] 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 ee49ac2..fd0ccba 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 7f1d0e7..3871d47 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 ec24ea7..0288671 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/251] 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 01c41ad..f0d8544 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 63d7b00..32a9bb0 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 9a5f667..848b765 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 e7c3cec..282c889 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 d4506a3..0000000 --- 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 8a6f621..0000000 --- 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 b902576..0000000 --- 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/251] 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 fd0ccba..000572e 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 3871d47..4e20f73 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 0288671..d48ee69 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/251] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index f0d8544..f8cfc70 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/251] 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 f8cfc70..2b23ced 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 32a9bb0..63d7b00 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 848b765..9a5f667 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 282c889..e7c3cec 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 0000000..d4506a3 --- /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 0000000..8a6f621 --- /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 0000000..b902576 --- /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/251] 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 000572e..b069996 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 4e20f73..ad8a73b 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 d48ee69..3ee690e 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/251] 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 2b23ced..e44b3a1 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 63d7b00..33435d9 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 9a1ceca..8405280 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 425fffd..f4d451c 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 4374485..0000000 --- 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/251] 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 e44b3a1..24a77a3 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 33435d9..63d7b00 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 8405280..a3e364a 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 f4d451c..425fffd 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 0000000..eb1ded7 --- /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/251] 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 b069996..08e82c4 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 ad8a73b..0440171 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 3ee690e..1f7f272 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/251] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 24a77a3..b26bed3 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/251] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index b26bed3..b30a966 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/251] 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 b30a966..bf7a932 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 b9a0ae8..6084c02 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/251] 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 08e82c4..3d2ac0b 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 6084c02..df3236b 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 0440171..ac353b9 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 c55ebbc..7b344b4 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 1f7f272..5d07ad9 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/251] 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 bf7a932..b26bed3 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 df3236b..cd2df75 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/251] 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 3d2ac0b..10f3091 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 ac353b9..034e116 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 5d07ad9..4f726fa 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/251] chore(docs): grammar improvements --- SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SECURITY.md b/SECURITY.md index bd2ba47..0c6c32d 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/251] 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 b26bed3..36c603b 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 cd2df75..28474ed 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 2aa307a..33b0cfd 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 0944a61..e1f9047 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 647dfc8..a992ef6 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 e84bb01..7dd69e6 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 91fc83e..d4d7a07 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/251] 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 36c603b..77c3857 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 63d7b00..e94025b 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 33b0cfd..6816edd 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 e7c3cec..d6ca955 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 e1f9047..14fd5fe 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 a992ef6..f44f336 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 0000000..4c5b1c6 --- /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 0000000..d3e90d5 --- /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 0000000..9c6bfc7 --- /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 0000000..b483291 --- /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 7dd69e6..8676b53 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 d4d7a07..ae2faf5 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/251] 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 77c3857..c728bcd 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 28474ed..a940dbb 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 ae2faf5..260d171 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 00d7a01..be1e7a1 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/251] 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 c728bcd..d4463c3 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 a940dbb..9d3c4fa 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 be1e7a1..38e2d56 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/251] 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 10f3091..6b7b74c 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 034e116..d4bf1e3 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 4f726fa..bd4f519 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/251] 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 d4463c3..be606c6 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 e94025b..cbacba5 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 4401501..3f1f495 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 425fffd..f4bf7a2 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 a97a2c5..01035ae 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 0000000..72ccf5d --- /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 0000000..c30fc16 --- /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 61af031..87dc31f 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/251] 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 6b7b74c..da59f99 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 d4bf1e3..ed0ec2d 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 bd4f519..e201745 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/251] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index be606c6..449c1d2 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/251] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 449c1d2..928da34 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/251] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 928da34..cd34424 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/251] 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 cd34424..dbb369a 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 9a5f667..3769bd5 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 a3e364a..98d1728 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 3f1f495..b5413d4 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 6816edd..375723a 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/251] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index dbb369a..03ed944 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/251] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 03ed944..a9ddb6d 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/251] 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 c486484..f05c930 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/251] 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 34308dd..785adea 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 798956f..4f21498 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 2b0c5c3..18a1ef5 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 38e2d56..575e4a4 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/251] 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 a9ddb6d..d654666 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 375723a..e3dc833 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 14fd5fe..e50aefb 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 260d171..4593d2f 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/251] 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 da59f99..2aca35a 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 ed0ec2d..4c9fc23 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 e201745..2c947c2 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/251] 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 4c9fc23..ce8b48d 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 efd90ea..f40d985 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/251] 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 785adea..c86e919 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/251] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index d654666..d2422bd 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/251] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index d2422bd..8826a54 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/251] 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 8826a54..f34bfc3 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 cbacba5..db76da3 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 bf6fbb4..084d2a5 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 647bde6..f65d1db 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 0000000..6442ff0 --- /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 d6ca955..93a1ee8 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 0000000..6701c0a --- /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 0000000..0f5d2b2 --- /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 0000000..09f1abc --- /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 0000000..efe9f7b --- /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 0000000..4bd80fc --- /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/251] 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 f34bfc3..f219d4b 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 eb1ded7..cee006c 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 09f1abc..59830fa 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/251] 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 f219d4b..c68e415 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 59830fa..60860c1 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/251] 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 c68e415..3f66d22 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 8a6f621..1d35fd2 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 0f5d2b2..c14bf27 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 60860c1..bcc98a0 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 efe9f7b..28c0d4b 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/251] 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 3f66d22..4dea91f 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 bcc98a0..757b51a 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/251] 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 4dea91f..3ea11ab 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 757b51a..44a17a6 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/251] 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 3ea11ab..3493617 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 44a17a6..58203f8 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/251] 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 3493617..bb23445 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 58203f8..44a17a6 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/251] 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 575e4a4..25c8f4a 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/251] 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 6d3cc20..3a11d3f 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/251] 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 51b16df..c3f5bc4 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/251] 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 bb23445..b912099 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 db76da3..9a7d9a7 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 084d2a5..63a7dc9 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 f65d1db..3b6a4d6 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 5602ad7..6ce731d 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 3769bd5..726db20 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 b5413d4..c87b8d7 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 93a1ee8..4a5c229 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 f4bf7a2..93aed9d 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 cee006c..fae1b2b 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 44a17a6..38afcad 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 0000000..572d51b --- /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 01035ae..1d6bc64 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 df4a166..d58f262 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 0000000..b1693a1 --- /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 f328b14..6626b53 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 0000000..6f30ea6 --- /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 c30fc16..e0029a9 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 0000000..1f139b0 --- /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 0000000..24e655f --- /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 0000000..69dbc56 --- /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 87dc31f..c11ea7c 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/251] 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 b912099..763dad3 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 9a7d9a7..b3f657c 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 4a5c229..ff41497 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 38afcad..e95ce26 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 b1693a1..1abbafc 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 1f139b0..f7c487b 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 0000000..47c8aae --- /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 0000000..542634b --- /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/251] 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 763dad3..1e9f62b 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 1abbafc..e3d7e8e 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/251] 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 1e9f62b..71dae95 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 b3f657c..4574ce7 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 ff41497..87e9ac8 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 f7c487b..45c73b3 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 47c8aae..0000000 --- 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 542634b..172a8be 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/251] 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 71dae95..ba1c7c9 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 4574ce7..cb25dcb 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 87e9ac8..cf2dbbc 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 45c73b3..e444e22 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 172a8be..0041b89 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 0000000..6cb4811 --- /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/251] chore(readme): update badges --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9d3c4fa..fa501ab 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/251] 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 25c8f4a..11e8a84 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/251] 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 2aca35a..4208b5c 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 ce8b48d..3bfd297 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 2c947c2..c6e626d 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/251] 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 ba1c7c9..4a84456 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 cb25dcb..0127e61 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 c87b8d7..3de46d0 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 c11ea7c..4fbd460 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/251] 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 4208b5c..ac03171 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 3bfd297..e783ee3 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 c6e626d..23bc576 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/251] 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 fa501ab..d5f8c84 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/251] 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 d5f8c84..00a4d1b 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 e783ee3..8e5e257 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 f40d985..27de013 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 4071919..4006aa2 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 4c0f254..4ad2b38 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 c86e919..c90f227 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 0a93c44..7b613c8 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 b902576..05066cd 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 4593d2f..91a9429 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 4bd80fc..954bc94 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 4fbd460..e739e44 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 3a11d3f..c860af0 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/251] 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 11e8a84..1c1160e 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/251] 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 4a84456..b296d07 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 0127e61..c8f114f 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 6442ff0..f27c894 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 cf2dbbc..c816cf1 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 1d35fd2..cf8463e 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 fae1b2b..6a19696 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 0000000..861f161 --- /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 e95ce26..00f0685 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 e3d7e8e..2effbde 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 e444e22..60a8de8 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 0000000..d5ca811 --- /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 954bc94..f68c9b0 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/251] 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 ac03171..e3778b2 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 8e5e257..e5375bc 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 23bc576..f175528 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/251] 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 b296d07..60f163b 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 00a4d1b..8ee8a72 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 e3dc833..0cbe820 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 e50aefb..2153abf 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 91a9429..f4111fa 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 1c1160e..e58799c 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/251] 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 e3778b2..5c87ad8 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 e5375bc..cec894e 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 f175528..8903bb2 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/251] =?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 47a8dca..b845b0f 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/251] 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 60f163b..9625f4e 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 c8f114f..49538b6 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 f27c894..d54c4ec 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 c816cf1..c89d7d5 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 cf8463e..bdbc3e6 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 f801195..8696a0f 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 00f0685..ca3c512 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 0000000..05704a1 --- /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 0000000..ba7759d --- /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 60a8de8..ea360f1 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 0000000..3d71136 --- /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 f68c9b0..3221416 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/251] 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 9625f4e..731478a 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 49538b6..ab8cb4f 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 d54c4ec..f27c894 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 c89d7d5..a0b086d 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 bdbc3e6..cf8463e 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 8696a0f..f801195 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 05704a1..0000000 --- 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 ba7759d..0000000 --- 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 3221416..f68c9b0 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/251] 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 731478a..9625f4e 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 ab8cb4f..49538b6 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 f27c894..d54c4ec 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 a0b086d..c89d7d5 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 cf8463e..bdbc3e6 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 f801195..8696a0f 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 0000000..05704a1 --- /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 0000000..ba7759d --- /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 f68c9b0..3221416 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/251] 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 5c87ad8..12aa896 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 cec894e..0c6e6ab 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 8903bb2..e0e69ec 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/251] 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 c3f5bc4..596c371 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/251] 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 596c371..d53ac62 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/251] 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 d53ac62..1692944 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 7b344b4..14b2cc8 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/251] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 9625f4e..d45a618 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/251] 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 d45a618..401ed65 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 0cbe820..56a95f0 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 2153abf..746a92f 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 f44f336..afba2b3 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 d3e90d5..43c8d92 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 8676b53..45cf74b 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 f4111fa..8f990be 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/251] 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 27de013..f4090db 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 4006aa2..c125a9e 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/251] 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 401ed65..aa1aff7 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 49538b6..8a0fbd3 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 56a95f0..01b529b 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 746a92f..7019e53 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 afba2b3..4fc470b 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 43c8d92..702c69b 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 45cf74b..8f44ddb 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 8f990be..a9cb8a5 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/251] 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 12aa896..1bc5713 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 0c6e6ab..a5baacd 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 e0e69ec..c3ceacf 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/251] 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 f4090db..55681a9 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 c125a9e..61c4c7a 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/251] 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 a5baacd..e97bd2f 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/251] 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 4f21498..528d568 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 4f41217..1e7293a 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/251] 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 8ee8a72..6f5f6ff 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/251] 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 e97bd2f..47a3ae2 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 c90f227..a654874 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 e58799c..86a8790 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/251] 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 6f5f6ff..5041da0 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/251] 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 aa1aff7..a0553b1 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 8a0fbd3..58ceeac 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 63a7dc9..0051076 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 0000000..236b5f7 --- /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 01b529b..b44573e 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 0000000..b801e8f --- /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 7019e53..746a92f 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 4fc470b..afba2b3 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 702c69b..43c8d92 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 8f44ddb..45cf74b 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 0000000..61b18bc --- /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 0000000..f53dd4d --- /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 0000000..d668386 --- /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 0000000..dd837d5 --- /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 0000000..fd8019a --- /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 0000000..930d008 --- /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 a9cb8a5..8f990be 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/251] 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 1bc5713..6538ca9 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 47a3ae2..0eb2b24 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 c3ceacf..73e122d 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/251] 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 a0553b1..38d124b 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 5041da0..884d10c 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 58ceeac..434ace2 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 0051076..a0b9ec2 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 726db20..652235e 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 6ce731d..0000000 --- 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 98d1728..0000000 --- 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 93aed9d..0000000 --- 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 cd1a7b5..0000000 --- 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 8696a0f..0000000 --- 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 6a19696..0000000 --- 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 fd8019a..0000000 --- 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 7b613c8..0000000 --- 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/251] 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 6538ca9..2b28d6e 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 0eb2b24..83d7c72 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 73e122d..8e3e520 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/251] 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 528d568..ffcbf67 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/251] 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 ffcbf67..b8387ce 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 1e7293a..72f55a8 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/251] 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 38d124b..5c1470b 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 d58f262..21fbcf3 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/251] 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 2b28d6e..34dc535 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 83d7c72..5a5d20e 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 8e3e520..0401c33 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/251] 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 8779740..95ceb18 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 0000000..5b01030 --- /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/251] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 5c1470b..fbe1e82 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/251] 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 a654874..79cd090 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 63dab8a..9a6dd19 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/251] 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 fbe1e82..c8304ed 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 884d10c..87e2b84 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 6ddf2c7..50a26f7 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/251] 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 c8304ed..fbe1e82 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 87e2b84..884d10c 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 50a26f7..6ddf2c7 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/251] 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 34dc535..a3bdfd2 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 5a5d20e..5927c84 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 0401c33..98240d7 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/251] 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 5927c84..a4c4b29 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/251] 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 fbe1e82..062204b 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 434ace2..f03855d 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 236b5f7..41452e9 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 b44573e..6d29c9e 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 0000000..8195b3f --- /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 0000000..3563c7c --- /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 0000000..a35e0de --- /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 61b18bc..c4e80a8 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 0000000..20924f3 --- /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 0000000..8f5a086 --- /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 0000000..d79bb8a --- /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 0000000..9ddf41e --- /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 0000000..7da1574 --- /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 0000000..87026f5 --- /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 0000000..9fca14b --- /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 0000000..d324cc9 --- /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 0000000..ee5d2e9 --- /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 0000000..5a02c1e --- /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 0000000..557eac1 --- /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 0000000..ebd13d9 --- /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 0000000..8df2f50 --- /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 0000000..5afddb1 --- /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 0000000..b9f78e4 --- /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 0000000..fd8019a --- /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 0000000..b815c8a --- /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 0000000..c82e09d --- /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/251] 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 a3bdfd2..6d78745 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 a4c4b29..49f04c9 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 98240d7..a21b043 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/251] 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 d2814ae..0b28f6e 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/251] 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 2b87845..dbeda2d 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/251] 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 062204b..86b4afd 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 6d29c9e..559e094 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 746a92f..140dac0 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 afba2b3..7b7b2ab 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 43c8d92..22fe230 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 45cf74b..74084b5 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 8f990be..38020d4 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/251] 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 b815c8a..683e154 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 c82e09d..24860c9 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 930d008..df1fed5 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 05066cd..5e6db3b 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 38020d4..6f9437f 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 3221416..c177978 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 e739e44..852f7f9 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/251] 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 86b4afd..f6de080 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 7b7b2ab..145b267 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 22fe230..b0157a1 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 74084b5..f0ab7a5 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/251] 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 6d78745..0598874 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 49f04c9..ddb4fbb 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 a21b043..de748b7 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/251] 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 1692944..fd67356 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/251] 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 fd67356..795bdad 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/251] 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 b8387ce..92f7c10 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/251] chore(internal): update pyright exclude list --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index ddb4fbb..584c367 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/251] 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 f6de080..b791078 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 f03855d..7e07a7b 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 41452e9..97c987e 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 559e094..80afc60 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 3563c7c..cb7b301 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 0000000..fbbe14a --- /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 0000000..d3f5eca --- /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 c4e80a8..d0b6b38 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 0000000..88212c6 --- /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 0000000..8f6534b --- /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 0000000..4646e05 --- /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 0000000..2eeb9b3 --- /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 0000000..3dd3ad5 --- /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 0000000..02588de --- /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 0000000..84be277 --- /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 0000000..ed128a7 --- /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 0000000..a468c47 --- /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 0000000..23444da --- /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 0000000..67626fe --- /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 0000000..9ece9a5 --- /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 0000000..d137a96 --- /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 0000000..0b1d0a8 --- /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 24860c9..38e07b7 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 0000000..6aac62f --- /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 0000000..6997762 --- /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/251] 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 0598874..091cfb1 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 584c367..3cc0fb3 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 de748b7..db4afd4 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/251] 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 18a1ef5..48bae95 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 d4fda26..ca547ce 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 1bac954..845cd6b 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 d81c8f4..3e90233 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/251] 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 b0cc20a..f0bcefd 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 d3f5eca..9fd6dd6 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 3dd3ad5..a6481c1 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 a468c47..8e901cb 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/251] 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 79cd090..0d2ff45 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 92d9ee6..bdef67f 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 92f7c10..3a6017e 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 ca547ce..dc64e29 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 0000000..dd70323 --- /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 0000000..7cb9d9e --- /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 f0bcefd..c19124f 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 845cd6b..193109f 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 ea3cf3f..f081859 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 72f55a8..ff5955d 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 a418f4f..5f7ab31 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 0000000..f626532 --- /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 3e90233..3147457 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/251] 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 b791078..6ac19ba 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 7e07a7b..7829585 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 a0b9ec2..3b4235c 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 3b6a4d6..964da37 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 80afc60..7394f21 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 0000000..0818cdc --- /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 c89d7d5..3c17469 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 140dac0..b9bb330 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 145b267..a1bc00e 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 b0157a1..08ddbd5 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 f0ab7a5..fc4c839 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 0000000..3ec5890 --- /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 0000000..0b2b12a --- /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 0000000..24b2744 --- /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 6f9437f..be4344f 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 0000000..6c97855 --- /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/251] 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 091cfb1..f7014c3 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 3cc0fb3..f731ab3 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 db4afd4..1caec76 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/251] 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 0745431..0000000 --- a/mypy.ini +++ /dev/null @@ -1,50 +0,0 @@ -[mypy] -pretty = True -show_error_codes = True - -# Exclude _files.py because mypy isn't smart enough to apply -# the correct type narrowing and as this is an internal module -# it's fine to just use Pyright. -# -# We also exclude our `tests` as mypy doesn't always infer -# types correctly and Pyright will still catch any type errors. -exclude = ^(src/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 f731ab3..2acfd24 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/251] 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 6ac19ba..8943c2f 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 884d10c..c5b5bf7 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 7829585..56d852b 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 0000000..2002d5e --- /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 d54c4ec..a288798 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 05704a1..b39b446 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 ba7759d..d22b007 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 c177978..97a90a8 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/251] 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 8943c2f..9635bb2 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 a288798..5c7715d 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 b39b446..54124da 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 97a90a8..fc5d299 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/251] 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 9635bb2..7fb3d31 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 c5b5bf7..beec3f0 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 2002d5e..cdf83c2 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/251] 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 2acfd24..0486b41 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 55681a9..56d0acc 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 86a8790..15329ae 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/251] 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 f7014c3..e82003f 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 0486b41..5533264 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 1caec76..0e02001 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/251] 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 56d0acc..249acb1 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 61c4c7a..49cc905 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 3a6017e..6a3cd1d 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/251] 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 4ad2b38..d1fdcc0 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 0d2ff45..756e21e 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 3b4235c..830aeb5 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 274320c..ada6fd3 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 48bae95..2c1d83b 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 c19124f..5207549 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 f081859..50d5926 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 652235e..28117b9 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 7394f21..e871c21 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 cb7b301..ff0cc48 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 a35e0de..ad26f2a 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 fbbe14a..1fd291d 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 9fd6dd6..2bdaeeb 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 b801e8f..9f15554 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 5c7715d..1581244 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 3de46d0..2a3848a 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 0818cdc..8d51da3 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 5f7ab31..68ca9b2 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/251] 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 e84fe62..b430fee 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/251] 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 be4344f..f463cf6 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 852f7f9..38734f8 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/251] 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 7fb3d31..385372f 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 56d852b..21ccdb7 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 830aeb5..2821af6 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 964da37..23b6b07 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 e871c21..145d083 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 2a3848a..8b5c1bc 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 0000000..886c423 --- /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 3c17469..e571af6 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 b9bb330..ed65be6 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 0000000..7af31f4 --- /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 0000000..42be61d --- /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 0000000..99367c8 --- /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 0000000..cb4b464 --- /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 f463cf6..349d74b 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 0000000..de295bf --- /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/251] 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 e82003f..95e4ab6 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 5533264..f6c5835 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 0e02001..a0cd88a 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/251] 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 385372f..4039b10 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 21ccdb7..eaef507 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 8b5c1bc..4d67164 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 e571af6..0eae67c 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 0000000..6784781 --- /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 38734f8..ae3b451 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/251] 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 95e4ab6..5332797 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 f6c5835..e5bcc8f 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 a0cd88a..408c4e4 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/251] 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 4039b10..be90d46 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 eaef507..dc0a70f 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 4d67164..ed10cf2 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 0eae67c..b14918e 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 0000000..06f75ff --- /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 0000000..4c26585 --- /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 ae3b451..1abf41d 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/251] 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 5332797..536ca31 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 e5bcc8f..c45dd07 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 408c4e4..acdc658 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/251] 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 be90d46..81ff967 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 ed10cf2..073314f 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/251] 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 81ff967..2b8a4b6 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 073314f..4c7e781 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 06f75ff..9673f2d 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 4c26585..e635b4d 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 6626b53..580424e 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 6f30ea6..48a2fa3 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 e0029a9..3bcc8bc 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 1abf41d..d36ea25 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/251] 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 536ca31..d87cca6 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 c45dd07..4001f30 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 acdc658..a8e88a1 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/251] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 2b8a4b6..1127c15 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/251] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 1127c15..4ada007 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/251] 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 4ada007..a1c1d77 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 a1bc00e..9b14e42 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 08ddbd5..9c32720 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 fc4c839..29ce4c1 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/251] 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 d87cca6..a713055 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 4001f30..04817a1 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 a8e88a1..77b8fbd 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/251] 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 a1c1d77..2223539 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 42be61d..d7759a7 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 99367c8..206e3b4 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 cb4b464..fe985d6 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/251] 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 2223539..0af2575 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 886c423..ba6862f 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 7af31f4..beb8a23 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 d7759a7..84290af 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 206e3b4..cd804f3 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 fe985d6..f70ea70 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 de295bf..484848f 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/251] 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 a713055..d52d2b9 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 04817a1..a44ee3c 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 77b8fbd..eed1006 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/251] 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 0af2575..b296ff8 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 dc0a70f..be7fff4 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 2821af6..ea9e51b 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 23b6b07..1b68d89 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 145d083..5e5530b 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 0000000..2f86871 --- /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 b14918e..bc28375 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 ed65be6..4a1104c 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 0000000..ab0363c --- /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 0000000..e9ca538 --- /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 0000000..c8c99e7 --- /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 0000000..d36dde3 --- /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 0000000..373e886 --- /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 349d74b..c3e7a7f 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 0000000..5d61f32 --- /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/251] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index b296ff8..f577dd0 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/251] 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 d52d2b9..a26ebfc 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 a44ee3c..3219e08 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 eed1006..0bebaf1 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/251] 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 f577dd0..136236a 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 beb8a23..1f8d4b7 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 84290af..831c45f 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 cd804f3..9648881 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 f70ea70..4c2d63c 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/251] 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 3219e08..b06cd8f 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/251] 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 136236a..9960955 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 be7fff4..6059a78 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 5e5530b..3e3eb8d 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 bc28375..6b49cf7 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 4a1104c..a0214a5 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 9b14e42..d7ef603 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 9c32720..22d72e1 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 ab0363c..6212380 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 29ce4c1..2da39af 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 c3e7a7f..e8ee60a 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/251] 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 a26ebfc..54c4d98 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 b06cd8f..f3f7874 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 0bebaf1..365119e 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/251] 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 9960955..6bb4af8 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 3e3eb8d..1d44421 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 a0214a5..23c3bb8 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 d7ef603..bcc5045 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 22d72e1..a1b332f 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 2da39af..f233929 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 e8ee60a..bd75630 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/251] 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 54c4d98..3d26904 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 f3f7874..c2e14c5 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 365119e..dfcce59 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/251] 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 6bb4af8..2bd40cc 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 beec3f0..ae0066e 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 6059a78..9f21935 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 1581244..bdc200f 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 2f86871..45d08d9 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 6b49cf7..2edc0bc 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 6701c0a..16eb570 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 d36dde3..6bb2b39 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 373e886..c4fd630 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 fc5d299..6c3354e 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 5d61f32..ffdb02f 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/251] 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 2bd40cc..b4dc606 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 9f21935..858dbfd 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 97c987e..abcc8f7 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 1d44421..c65a738 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 0000000..68cee42 --- /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 45d08d9..2f86871 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 2edc0bc..6b49cf7 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 d0b6b38..9b0ed53 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 0000000..942cef3 --- /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 0000000..9bde2e6 --- /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 0000000..fb03b4b --- /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 0000000..1769e07 --- /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 0000000..ea2c9b4 --- /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 0000000..110cb30 --- /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 0000000..3a2c513 --- /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 6bb2b39..d36dde3 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 c4fd630..373e886 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 0000000..9e24548 --- /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 ffdb02f..5d61f32 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/251] 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 3d26904..8f3e0a4 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 c2e14c5..0f8cb14 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 dfcce59..e65ca7f 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/251] 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 0f8cb14..0c6c3eb 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 249acb1..fc03273 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 49cc905..ed64e3d 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/251] 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 b4dc606..8cd6a7c 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 858dbfd..164a8c4 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 abcc8f7..a1acee2 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 c65a738..6a0129c 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 0000000..c168a4a --- /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 9b0ed53..f8a263c 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 0000000..948a74c --- /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 0000000..a805ba8 --- /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 0000000..cb79410 --- /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/251] 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 8f3e0a4..b4e9013 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 0c6c3eb..18317f8 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 e65ca7f..211e253 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/251] 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 8cd6a7c..8cf9ee7 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 1f8d4b7..485df60 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 831c45f..6ec2f7f 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 9648881..e4abb0d 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 4c2d63c..5262fc4 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/251] 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 b4e9013..6db19b9 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 18317f8..1a56293 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 211e253..123dd30 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/251] 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 e3131a3..e6d0306 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/251] 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 8cf9ee7..9e4dbe6 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 164a8c4..aa2ef0e 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 28117b9..34aa129 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 d4506a3..1e2f527 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 bdbc3e6..56a2d4b 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 5e6db3b..7475bcd 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/251] 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 6db19b9..4ad3fef 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 1a56293..737af0a 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 123dd30..abb53bd 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/251] 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 15329ae..95661e8 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/251] 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 50d5926..eec7f4a 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/251] 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 9e4dbe6..0080fd6 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 34aa129..b803299 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 1e2f527..296ded5 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/251] 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 ae0066e..699f3c2 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 737af0a..52ba59c 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 ad7ec71..f6027c1 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/251] 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 6a3cd1d..fcec2cf 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 ff5955d..78f0fd3 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/251] 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 fcec2cf..ca9500b 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/251] 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 0080fd6..125a84b 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 aa2ef0e..8a015c3 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 68cee42..87d377f 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 f8a263c..546fdc6 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 0000000..f003ee9 --- /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 0000000..c82302e --- /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 9e24548..7634b89 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/251] 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 4ad3fef..e756293 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 52ba59c..518a22e 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 abb53bd..dea2694 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/251] 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 125a84b..b12b071 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 6a0129c..305cc51 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 23c3bb8..4994f2a 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 bcc5045..26fef74 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 a1b332f..72ab6f3 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 f233929..4d575f0 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/251] 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 e756293..96dfab3 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 518a22e..0dede54 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 dea2694..3a441bd 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/251] 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 b12b071..53123c9 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 305cc51..8503736 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 4994f2a..1e54ce7 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/251] 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 96dfab3..c18333e 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 0dede54..fc74c8a 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 3a441bd..67d5101 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/251] 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 53123c9..11b820d 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 8a015c3..afa0ddd 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 8503736..84a4fe4 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 6b49cf7..208a8bd 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 26fef74..21041ea 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 0000000..20837be --- /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 72ab6f3..7497869 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 4d575f0..527386d 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 bd75630..c87fc3d 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/251] 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 c18333e..0c2ecec 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 fc74c8a..b5d3ecb 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 67d5101..1a28cba 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/251] 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 b5d3ecb..4313956 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/251] 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 11b820d..bdbede3 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 afa0ddd..483ff9c 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 ea9e51b..ba42433 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 1b68d89..233ef50 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 0000000..cb159eb --- /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 0000000..b7bb580 --- /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 0000000..d985320 --- /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 0000000..2e09909 --- /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 0000000..6ea0940 --- /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 0000000..e8c2277 --- /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 0000000..000bdec --- /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 0000000..0ec0b0b --- /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 0000000..2855fc2 --- /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 0000000..c57002f --- /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 0000000..78a13a3 --- /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 0000000..1a23b25 --- /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 0000000..347c57c --- /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 0000000..efaf9ea --- /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 0000000..6e0f0c8 --- /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 0000000..90a4864 --- /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 0000000..fd8019a --- /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 0000000..fd8019a --- /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 0000000..25fbdb1 --- /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 0000000..32d2784 --- /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/251] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index bdbede3..fa4f900 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/251] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index fa4f900..541095f 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/251] 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 e6d0306..369a3f6 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/251] 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 4313956..71c4c9a 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 fc03273..b435fc7 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 ed64e3d..646e8cd 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/251] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 541095f..a6b35ee 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/251] 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 a6b35ee..c32f96d 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 483ff9c..fe6b45c 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 ba42433..37ba489 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 233ef50..cf08046 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 cb159eb..0000000 --- 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 b7bb580..0000000 --- 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 d985320..0000000 --- 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 2e09909..0000000 --- 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 6ea0940..0000000 --- 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 0000000..d085d51 --- /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 84a4fe4..f41b46a 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 208a8bd..45b0c4b 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 e8c2277..0000000 --- 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 000bdec..0000000 --- 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 0ec0b0b..0000000 --- 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 2855fc2..0000000 --- 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 c57002f..0000000 --- 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 78a13a3..0000000 --- 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 1a23b25..0000000 --- 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 347c57c..0000000 --- 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 efaf9ea..0000000 --- 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 6e0f0c8..0000000 --- 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 90a4864..0000000 --- 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 1e54ce7..d1ff790 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 21041ea..0a5f33d 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 7497869..d639729 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 0000000..5fd30dc --- /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 0000000..d0df921 --- /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 0000000..76ad037 --- /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 0000000..c7f87c6 --- /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 0000000..0a63c0f --- /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 0000000..a11c4de --- /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 0000000..104b0b0 --- /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 0000000..c25b3a5 --- /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 0000000..ed9a7e8 --- /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 527386d..111149b 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 ea360f1..6b64919 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 0000000..7bc1a5f --- /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 0000000..5f790cc --- /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 0000000..abffcc2 --- /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 0000000..de63c64 --- /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 0000000..d81ac70 --- /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 0000000..e1027d2 --- /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 0000000..b7cb2f0 --- /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 fd8019a..0000000 --- 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 fd8019a..0000000 --- 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 25fbdb1..0000000 --- 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 32d2784..0000000 --- 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 0000000..6a8f164 --- /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/251] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index c32f96d..25e77f1 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/251] 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 0c2ecec..86b0e83 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 71c4c9a..f4bbe65 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 1a28cba..1bd01d6 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/251] 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 f4bbe65..59d67db 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 b435fc7..7643dfb 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 646e8cd..bbfe2b3 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/251] 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 699f3c2..d5905ea 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/251] 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 25e77f1..7792bb0 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 4c7e781..fa808dd 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 1d6bc64..288656a 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 d36ea25..40c0545 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/251] 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 86b0e83..cb9d254 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 59d67db..facf999 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 1bd01d6..c911504 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/251] 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 7792bb0..a0095f1 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 d5905ea..6d1dd19 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 f41b46a..30888da 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 d1ff790..4a5f479 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 0a5f33d..344549e 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 d639729..cfa2ce5 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 9c6bfc7..5c362ee 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 b483291..bbd9e48 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 76ad037..1bb69e8 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 111149b..2b49d65 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 c87fc3d..a766656 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/251] 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 a0095f1..44c807a 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 d085d51..8c480ed 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 30888da..cbd1773 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 4a5f479..0818760 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 344549e..efff854 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 cfa2ce5..3ce2648 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 1bb69e8..4b70a87 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 c7f87c6..6c8e815 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 c25b3a5..c54fad4 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 ed9a7e8..2cd3be7 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 2b49d65..12f58a5 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/251] =?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 44c807a..0c474bb 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 fe6b45c..d6384dc 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 37ba489..c941be7 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 cf08046..5de2a85 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 0000000..cb159eb --- /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 0000000..b7bb580 --- /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 0000000..6130549 --- /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 0000000..daa8221 --- /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 0000000..15729ed --- /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 0000000..1fdcc09 --- /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 0000000..000bdec --- /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 0000000..82b5f80 --- /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 0000000..3287ba0 --- /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 0000000..c57002f --- /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 0000000..bfbd280 --- /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 0000000..aa03f0c --- /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 0000000..71e4d18 --- /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 0000000..91b74ce --- /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 0000000..be92e7d --- /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 0000000..8671f97 --- /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 0000000..9c9fb35 --- /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 0000000..d1b9dc9 --- /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 0000000..fd8019a --- /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 0000000..fd8019a --- /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 0000000..e9c3d8f --- /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 0000000..0dc190d --- /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/251] =?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 0c474bb..4bf353a 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 d6384dc..2876b33 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 daa8221..b4fc758 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 15729ed..361f424 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 1fdcc09..686e805 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 bfbd280..0296883 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 0000000..b3de645 --- /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 3287ba0..0283c35 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 9c9fb35..fe57730 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 0000000..a4b2ffc --- /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 e9c3d8f..7957caf 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 0dc190d..5115f90 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/251] 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 2c1d83b..275ffbb 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/251] 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 000bdec..5e411dc 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 82b5f80..02b5ecf 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 c57002f..5ca9578 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 91b74ce..710d9c3 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 8671f97..ff9f5e9 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 0283c35..baa80e2 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 fe57730..7869925 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 d1b9dc9..0c6715c 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 56a2d4b..338f506 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 5c362ee..381d630 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 bbd9e48..6109abf 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 5fd30dc..ddd3d9f 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 c54fad4..a392b3f 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 c82302e..0e07023 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 8df2f50..5778a30 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 a805ba8..d53080d 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 02588de..a5e4b77 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 ed128a7..6706e88 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 23444da..0cda64d 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 67626fe..91c7724 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 d137a96..be3c798 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 0b1d0a8..6e911f5 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 f53dd4d..8cf9d54 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 dd837d5..ac4130b 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 16eb570..84d3d87 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 c14bf27..5746c97 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 ca3c512..d6de222 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 d22b007..d7719d4 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 28c0d4b..3601c86 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 572d51b..cc221c7 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 c8c99e7..79a5c99 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 373e886..1b3be22 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 48a2fa3..f32bf8e 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 3ec5890..e141aa0 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 485df60..0a3536f 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 6ec2f7f..dc474ab 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 e4abb0d..08c846f 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 5262fc4..24c7b96 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 3d71136..1babce1 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 7bc1a5f..a91d2dc 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 5f790cc..4aadc31 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 abffcc2..ab8f427 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 0041b89..35175f5 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 d5ca811..3745e9b 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 69dbc56..078b6ec 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 d81ac70..e6c2b8f 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 e1027d2..51187db 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 b7cb2f0..9236547 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/251] 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 4bf353a..135345a 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 ff9f5e9..423b92e 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/251] 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 cb9d254..7f3f5c8 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 facf999..6e76a58 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 c911504..6e51841 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/251] 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 756e21e..e55218b 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/251] =?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 135345a..0a02606 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 2876b33..d5c3bc6 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 c941be7..4dc1197 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 5de2a85..e6e8103 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 b4fc758..39f22fc 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 361f424..ac2bb8e 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 0000000..91dafce --- /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 45b0c4b..b5a9804 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 686e805..e2c3bb0 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 b3de645..b2727e0 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 423b92e..50f9149 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 baa80e2..da0b6f6 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 7869925..7cf7665 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 0000000..4ead46a --- /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 0000000..30def06 --- /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 0000000..3f7b2d9 --- /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 0000000..945909e --- /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 0000000..ffc0c1c --- /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 7957caf..eef21a9 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 5115f90..192361a 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 0000000..00c7635 --- /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/251] 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 4dc1197..166ecdb 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/251] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 0a02606..299cb5a 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/251] 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 299cb5a..99bb6c4 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 d5c3bc6..fb67de5 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 b5a9804..5fad216 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 ddd3d9f..1694313 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 a392b3f..0000000 --- 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/251] 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 7f3f5c8..d2d60a3 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 6e76a58..770392c 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 6e51841..17d46b5 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/251] 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 e55218b..787be54 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/251] 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 b5b8891..7675e60 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/251] 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 99bb6c4..91fb407 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 ac2bb8e..f5e6053 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 02b5ecf..3ddfe8e 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 da0b6f6..b2a5e20 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 4ead46a..4fbf0e4 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/251] 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 91fb407..579e071 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 fb67de5..848c104 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 ba6862f..4908ab7 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 5fad216..1748bf2 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 0000000..dc45f4f --- /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 484848f..ed858e8 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/251] chore(internal): codegen related update --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index b32a077..3b7d20d 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/251] 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 579e071..434275e 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 848c104..db5f2df 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 39f22fc..f4a0276 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 f5e6053..34ab614 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 91dafce..85e0c8a 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 1748bf2..0665e53 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 e2c3bb0..2ecdef6 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 5e411dc..0000000 --- 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 3ddfe8e..42b54a4 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 5ca9578..8cb0df1 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 0296883..41e8ba8 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 aa03f0c..0000000 --- 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 be92e7d..ad9f9c1 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 50f9149..33fc46b 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 b2a5e20..6027f4d 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 7cf7665..b792d56 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 a4b2ffc..52d5337 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 0c6715c..72ac294 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 4fbf0e4..0000000 --- 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 30def06..8ae733b 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 3f7b2d9..94964b9 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 0000000..670f4e7 --- /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 ffc0c1c..c42209e 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 eef21a9..1bae66d 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 192361a..9855ef8 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 00c7635..b609868 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/251] 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 120241d..994e625 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 5e7787d..ba1be2c 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 434275e..9ab4346 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 f05c930..9cb624f 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 6d1dd19..d3e4341 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 770392c..22aadee 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 9a6dd19..bbef8bf 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 b7bb580..6999bd5 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 f4a0276..4a541f7 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 34ab614..aa1c4da 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 b803299..0443e73 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 8c480ed..5a4bf61 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 cbd1773..8050a7d 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 87d377f..c23dd3d 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 ff0cc48..0da0bdd 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 ad26f2a..2a5c1e3 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 1fd291d..ab97a70 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 c168a4a..5c47e3b 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 2bdaeeb..f5c4341 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 9f15554..8a1d199 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 85e0c8a..30e72e8 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 bdc200f..f924531 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 2f86871..69497b1 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 fa808dd..3b812d4 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 8d51da3..86064d5 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 4908ab7..6574a25 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)