diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..62c2d13f5 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,8 @@ +ARG VARIANT="3.9" +FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} + +USER vscode + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +RUN echo "[[ -d .venv ]] && source .venv/bin/activate || export PATH=\$PATH" >> /home/vscode/.bashrc diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..e01283d8c --- /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": "uv sync --all-extras", + + "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": "${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 000000000..3a6e2ca5f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,93 @@ +name: CI +on: + push: + branches-ignore: + - 'generated' + - 'codegen/**' + - 'integrated/**' + - 'stl-preview-head/**' + - 'stl-preview-base/**' + pull_request: + branches-ignore: + - 'stl-preview-head/**' + - 'stl-preview-base/**' + +jobs: + lint: + timeout-minutes: 10 + name: lint + runs-on: ${{ github.repository == 'stainless-sdks/runloop-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + steps: + - uses: actions/checkout@v6 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + version: '0.10.2' + + - name: Install dependencies + run: uv sync --all-extras + + - name: Run lints + run: ./scripts/lint + + build: + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + timeout-minutes: 10 + name: build + permissions: + contents: read + id-token: write + runs-on: ${{ github.repository == 'stainless-sdks/runloop-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + steps: + - uses: actions/checkout@v6 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + version: '0.10.2' + + - name: Install dependencies + run: uv sync --all-extras + + - name: Run build + run: uv build + + - name: Get GitHub OIDC Token + if: |- + github.repository == 'stainless-sdks/runloop-python' && + !startsWith(github.ref, 'refs/heads/stl/') + id: github-oidc + uses: actions/github-script@v8 + with: + script: core.setOutput('github_token', await core.getIDToken()); + + - name: Upload tarball + if: |- + github.repository == 'stainless-sdks/runloop-python' && + !startsWith(github.ref, 'refs/heads/stl/') + env: + URL: https://pkg.stainless.com/s + AUTH: ${{ steps.github-oidc.outputs.github_token }} + SHA: ${{ github.sha }} + run: ./scripts/utils/upload-artifact.sh + + test: + timeout-minutes: 10 + name: test + runs-on: ${{ github.repository == 'stainless-sdks/runloop-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + steps: + - uses: actions/checkout@v6 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + version: '0.10.2' + + - name: Bootstrap + run: ./scripts/bootstrap + + - name: Run tests + run: ./scripts/test diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml new file mode 100644 index 000000000..bce59933f --- /dev/null +++ b/.github/workflows/publish-pypi.yml @@ -0,0 +1,28 @@ +# 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/runloopai/api-client-python/actions/workflows/publish-pypi.yml +name: Publish PyPI +on: + workflow_dispatch: + + release: + types: [published] + +jobs: + publish: + name: publish + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + version: '0.9.13' + + - name: Publish to PyPI + run: | + bash ./bin/publish-pypi + env: + PYPI_TOKEN: ${{ secrets.RUNLOOP_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml new file mode 100644 index 000000000..051116384 --- /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 == 'runloopai/api-client-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') + + steps: + - uses: actions/checkout@v6 + + - name: Check release environment + run: | + bash ./bin/check-release-environment + env: + PYPI_TOKEN: ${{ secrets.RUNLOOP_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..95ceb189a --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +.prism.log +_dev + +__pycache__ +.mypy_cache + +dist + +.venv +.idea + +.env +.envrc +codegen.log +Brewfile.lock.json diff --git a/.python-version b/.python-version new file mode 100644 index 000000000..43077b246 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.9.18 diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 000000000..caf148712 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "1.11.0" +} \ No newline at end of file diff --git a/.stats.yml b/.stats.yml new file mode 100644 index 000000000..a784436d5 --- /dev/null +++ b/.stats.yml @@ -0,0 +1,4 @@ +configured_endpoints: 118 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-132ed160716591bdcd9231c00da8c506d9451a5486b165fc27b2a01d93202082.yml +openapi_spec_hash: c2b44d9e9cda56e32141a7ea3794bbba +config_hash: 3bd89c812b96708c461fb98286ebf0b5 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..5b0103078 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.analysis.importFormat": "relative", +} diff --git a/Brewfile b/Brewfile new file mode 100644 index 000000000..c43041cef --- /dev/null +++ b/Brewfile @@ -0,0 +1,2 @@ +brew "uv" + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..9ac1ce602 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,127 @@ +## Setting up the environment + +### With `uv` + +We use [uv](https://docs.astral.sh/uv/) to manage dependencies because it will automatically provision a Python environment with the expected Python version. To set it up, run: + +```sh +$ ./scripts/bootstrap +``` + +Or [install uv manually](https://docs.astral.sh/uv/getting-started/installation/) and run: + +```sh +$ uv sync --all-extras +``` + +You can then run scripts using `uv run python script.py` or by manually activating the virtual environment: + +```sh +# manually activate - https://docs.python.org/3/library/venv.html#how-venvs-work +$ source .venv/bin/activate + +# now you can omit the `uv run` prefix +$ python script.py +``` + +### Without `uv` + +Alternatively if you don't want to install `uv`, you can stick with the standard `pip` setup by ensuring you have the Python version specified in `.python-version`, create a virtual environment however you desire and then install dependencies using this command: + +```sh +$ pip install -r requirements-dev.lock +``` + +## 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/runloop_api_client/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 uv 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/runloopai/api-client-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 +$ uv 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 +$ ./scripts/mock +``` + +```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/runloopai/api-client-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 000000000..59199f55e --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright 2026 runloop + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 35ac00c8a..9bc50dca5 100644 --- a/README.md +++ b/README.md @@ -1 +1,470 @@ -# runloop-python \ No newline at end of file +# Runloop Python API library + + +[![PyPI version](https://img.shields.io/pypi/v/runloop_api_client.svg?label=pypi%20(stable))](https://pypi.org/project/runloop_api_client/) + +The Runloop Python library provides convenient access to the Runloop REST API from any Python 3.9+ +application. The library includes type definitions for all request params and response fields, +and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx). + +It is generated with [Stainless](https://www.stainless.com/). + +## MCP Server + +Use the Runloop MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application. + +[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=%40runloop%2Fapi-client-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBydW5sb29wL2FwaS1jbGllbnQtbWNwIl0sImVudiI6eyJSVU5MT09QX0FQSV9LRVkiOiJNeSBCZWFyZXIgVG9rZW4ifX0) +[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40runloop%2Fapi-client-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40runloop%2Fapi-client-mcp%22%5D%2C%22env%22%3A%7B%22RUNLOOP_API_KEY%22%3A%22My%20Bearer%20Token%22%7D%7D) + +> Note: You may need to set environment variables in your MCP client. + +## Documentation + +The REST API documentation can be found on [runloop.ai](https://runloop.ai). The full API of this library can be found in [api.md](api.md). + +## Installation + +```sh +# install from PyPI +pip install runloop_api_client +``` + +## Usage + +The full API of this library can be found in [api.md](api.md). + +```python +import os +from runloop_api_client import Runloop + +client = Runloop( + bearer_token=os.environ.get("RUNLOOP_API_KEY"), # This is the default and can be omitted +) + +devbox_view = client.devboxes.create() +print(devbox_view.id) +``` + +While you can provide a `bearer_token` keyword argument, +we recommend using [python-dotenv](https://pypi.org/project/python-dotenv/) +to add `RUNLOOP_API_KEY="My Bearer Token"` to your `.env` file +so that your Bearer Token is not stored in source control. + +## Async usage + +Simply import `AsyncRunloop` instead of `Runloop` and use `await` with each API call: + +```python +import os +import asyncio +from runloop_api_client import AsyncRunloop + +client = AsyncRunloop( + bearer_token=os.environ.get("RUNLOOP_API_KEY"), # This is the default and can be omitted +) + + +async def main() -> None: + devbox_view = await client.devboxes.create() + print(devbox_view.id) + + +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 runloop_api_client[aiohttp] +``` + +Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: + +```python +import os +import asyncio +from runloop_api_client import DefaultAioHttpClient +from runloop_api_client import AsyncRunloop + + +async def main() -> None: + async with AsyncRunloop( + bearer_token=os.environ.get("RUNLOOP_API_KEY"), # This is the default and can be omitted + http_client=DefaultAioHttpClient(), + ) as client: + devbox_view = await client.devboxes.create() + print(devbox_view.id) + + +asyncio.run(main()) +``` + +## Using types + +Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev) which also provide helper methods for things like: + +- 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`. + +## Pagination + +List methods in the Runloop 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 runloop_api_client import Runloop + +client = Runloop() + +all_devboxes = [] +# Automatically fetches more pages as needed. +for devbox in client.devboxes.list(): + # Do something with devbox here + all_devboxes.append(devbox) +print(all_devboxes) +``` + +Or, asynchronously: + +```python +import asyncio +from runloop_api_client import AsyncRunloop + +client = AsyncRunloop() + + +async def main() -> None: + all_devboxes = [] + # Iterate through items across all pages, issuing requests as needed. + async for devbox in client.devboxes.list(): + all_devboxes.append(devbox) + print(all_devboxes) + + +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.devboxes.list() +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.devboxes)}") + +# Remove `await` for non-async usage. +``` + +Or just work directly with the returned data: + +```python +first_page = await client.devboxes.list() + +print(f"next page cursor: {first_page.starting_after}") # => "next page cursor: ..." +for devbox in first_page.devboxes: + print(devbox.id) + +# Remove `await` for non-async usage. +``` + +## Nested params + +Nested parameters are dictionaries, typed using `TypedDict`, for example: + +```python +from runloop_api_client import Runloop + +client = Runloop() + +devbox_view = client.devboxes.create( + launch_parameters={}, +) +print(devbox_view.launch_parameters) +``` + +## 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 runloop_api_client import Runloop + +client = Runloop() + +client.devboxes.upload_file( + id="id", + path="path", + file=Path("/path/to/file"), +) +``` + +The async client uses the exact same interface. If you pass a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance, the file contents will be read asynchronously automatically. + +## Handling errors + +When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `runloop_api_client.APIConnectionError` is raised. + +When the API returns a non-success status code (that is, 4xx or 5xx +response), a subclass of `runloop_api_client.APIStatusError` is raised, containing `status_code` and `response` properties. + +All errors inherit from `runloop_api_client.APIError`. + +```python +import runloop_api_client +from runloop_api_client import Runloop + +client = Runloop() + +try: + client.devboxes.create() +except runloop_api_client.APIConnectionError as e: + print("The server could not be reached") + print(e.__cause__) # an underlying Exception, likely raised within httpx. +except runloop_api_client.RateLimitError as e: + print("A 429 status code was received; we should back off a bit.") +except runloop_api_client.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 5 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 runloop_api_client import Runloop + +# Configure the default for all requests: +client = Runloop( + # default is 2 + max_retries=0, +) + +# Or, configure per-request: +client.with_options(max_retries=5).devboxes.create() +``` + +### Timeouts + +By default requests time out after 30 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 +from runloop_api_client import Runloop + +# Configure the default for all requests: +client = Runloop( + # 20 seconds (default is 30 seconds) + timeout=20.0, +) + +# More granular control: +client = Runloop( + timeout=httpx.Timeout(60.0, read=5.0, write=10.0, connect=2.0), +) + +# Override per-request: +client.with_options(timeout=5.0).devboxes.create() +``` + +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 `RUNLOOP_LOG` to `info`. + +```shell +$ export RUNLOOP_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 runloop_api_client import Runloop + +client = Runloop() +response = client.devboxes.with_raw_response.create() +print(response.headers.get('X-My-Header')) + +devbox = response.parse() # get the object that `devboxes.create()` would have returned +print(devbox.id) +``` + +These methods return an [`APIResponse`](https://github.com/runloopai/api-client-python/tree/main/src/runloop_api_client/_response.py) object. + +The async client returns an [`AsyncAPIResponse`](https://github.com/runloopai/api-client-python/tree/main/src/runloop_api_client/_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.devboxes.with_streaming_response.create() 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 runloop_api_client import Runloop, DefaultHttpxClient + +client = Runloop( + # Or use the `RUNLOOP_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 runloop_api_client import Runloop + +with Runloop() 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/runloopai/api-client-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 runloop_api_client +print(runloop_api_client.__version__) +``` + +## Requirements + +Python 3.9 or higher. + +## Contributing + +See [the contributing documentation](./CONTRIBUTING.md). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..6426d978b --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,27 @@ +# 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 Runloop, please follow the respective company's security reporting guidelines. + +### Runloop Terms and Policies + +Please contact support@runloop.ai for any questions or concerns regarding the security of our services. + +--- + +Thank you for helping us keep the SDKs and systems they interact with secure. diff --git a/api.md b/api.md new file mode 100644 index 000000000..23a3dee64 --- /dev/null +++ b/api.md @@ -0,0 +1,441 @@ +# Shared Types + +```python +from runloop_api_client.types import ( + AfterIdle, + AgentMount, + AgentSource, + CodeMountParameters, + LaunchParameters, + Mount, + ObjectMount, + RunProfile, +) +``` + +# Benchmarks + +Types: + +```python +from runloop_api_client.types import ( + BenchmarkCreateParameters, + BenchmarkScenarioUpdateParameters, + BenchmarkUpdateParameters, + BenchmarkView, + ScenarioDefinitionListView, + StartBenchmarkRunParameters, +) +``` + +Methods: + +- client.benchmarks.create(\*\*params) -> BenchmarkView +- client.benchmarks.retrieve(id) -> BenchmarkView +- client.benchmarks.update(id, \*\*params) -> BenchmarkView +- client.benchmarks.list(\*\*params) -> SyncBenchmarksCursorIDPage[BenchmarkView] +- client.benchmarks.definitions(id, \*\*params) -> ScenarioDefinitionListView +- client.benchmarks.list_public(\*\*params) -> SyncBenchmarksCursorIDPage[BenchmarkView] +- client.benchmarks.start_run(\*\*params) -> BenchmarkRunView +- client.benchmarks.update_scenarios(id, \*\*params) -> BenchmarkView + +# BenchmarkRuns + +Types: + +```python +from runloop_api_client.types import BenchmarkRunListView, BenchmarkRunView +``` + +Methods: + +- client.benchmark_runs.retrieve(id) -> BenchmarkRunView +- client.benchmark_runs.list(\*\*params) -> SyncBenchmarkRunsCursorIDPage[BenchmarkRunView] +- client.benchmark_runs.cancel(id) -> BenchmarkRunView +- client.benchmark_runs.complete(id) -> BenchmarkRunView +- client.benchmark_runs.list_scenario_runs(id, \*\*params) -> SyncBenchmarkRunsCursorIDPage[ScenarioRunView] + +# BenchmarkJobs + +Types: + +```python +from runloop_api_client.types import ( + BenchmarkJobCreateParameters, + BenchmarkJobListView, + BenchmarkJobView, +) +``` + +Methods: + +- client.benchmark_jobs.create(\*\*params) -> BenchmarkJobView +- client.benchmark_jobs.retrieve(id) -> BenchmarkJobView +- client.benchmark_jobs.list(\*\*params) -> BenchmarkJobListView + +# Agents + +Types: + +```python +from runloop_api_client.types import AgentCreateParameters, AgentListView, AgentView +``` + +Methods: + +- client.agents.create(\*\*params) -> AgentView +- client.agents.retrieve(id) -> AgentView +- client.agents.list(\*\*params) -> SyncAgentsCursorIDPage[AgentView] + +# Blueprints + +Types: + +```python +from runloop_api_client.types import ( + BlueprintBuildFromInspectionParameters, + BlueprintBuildLog, + BlueprintBuildLogsListView, + BlueprintBuildParameters, + BlueprintListView, + BlueprintPreviewView, + BlueprintView, + InspectionSource, +) +``` + +Methods: + +- client.blueprints.create(\*\*params) -> BlueprintView +- client.blueprints.retrieve(id) -> BlueprintView +- client.blueprints.list(\*\*params) -> SyncBlueprintsCursorIDPage[BlueprintView] +- client.blueprints.delete(id) -> object +- client.blueprints.create_from_inspection(\*\*params) -> BlueprintView +- client.blueprints.list_public(\*\*params) -> SyncBlueprintsCursorIDPage[BlueprintView] +- client.blueprints.logs(id) -> BlueprintBuildLogsListView +- client.blueprints.preview(\*\*params) -> BlueprintPreviewView + +# Devboxes + +Types: + +```python +from runloop_api_client.types import ( + DevboxAsyncExecutionDetailView, + DevboxExecutionDetailView, + DevboxKillExecutionRequest, + DevboxListView, + DevboxResourceUsageView, + DevboxSendStdInRequest, + DevboxSendStdInResult, + DevboxSnapshotListView, + DevboxSnapshotView, + DevboxTunnelView, + DevboxView, + TunnelView, + DevboxCreateSSHKeyResponse, + DevboxReadFileContentsResponse, +) +``` + +Methods: + +- client.devboxes.create(\*\*params) -> DevboxView +- client.devboxes.retrieve(id) -> DevboxView +- client.devboxes.update(id, \*\*params) -> DevboxView +- client.devboxes.list(\*\*params) -> SyncDevboxesCursorIDPage[DevboxView] +- client.devboxes.create_ssh_key(id) -> DevboxCreateSSHKeyResponse +- client.devboxes.create_tunnel(id, \*\*params) -> DevboxTunnelView +- client.devboxes.delete_disk_snapshot(id) -> object +- client.devboxes.download_file(id, \*\*params) -> BinaryAPIResponse +- client.devboxes.enable_tunnel(id, \*\*params) -> TunnelView +- client.devboxes.execute(id, \*\*params) -> DevboxAsyncExecutionDetailView +- client.devboxes.execute_async(id, \*\*params) -> DevboxAsyncExecutionDetailView +- client.devboxes.execute_sync(id, \*\*params) -> DevboxExecutionDetailView +- client.devboxes.keep_alive(id) -> object +- client.devboxes.list_disk_snapshots(\*\*params) -> SyncDiskSnapshotsCursorIDPage[DevboxSnapshotView] +- client.devboxes.read_file_contents(id, \*\*params) -> str +- client.devboxes.remove_tunnel(id, \*\*params) -> object +- client.devboxes.resume(id) -> DevboxView +- client.devboxes.retrieve_resource_usage(id) -> DevboxResourceUsageView +- client.devboxes.shutdown(id, \*\*params) -> DevboxView +- client.devboxes.snapshot_disk(id, \*\*params) -> DevboxSnapshotView +- client.devboxes.snapshot_disk_async(id, \*\*params) -> DevboxSnapshotView +- client.devboxes.suspend(id) -> DevboxView +- client.devboxes.upload_file(id, \*\*params) -> object +- client.devboxes.wait_for_command(execution_id, \*, devbox_id, \*\*params) -> DevboxAsyncExecutionDetailView +- client.devboxes.write_file_contents(id, \*\*params) -> DevboxExecutionDetailView + +## DiskSnapshots + +Types: + +```python +from runloop_api_client.types.devboxes import DevboxSnapshotAsyncStatusView +``` + +Methods: + +- client.devboxes.disk_snapshots.update(id, \*\*params) -> DevboxSnapshotView +- client.devboxes.disk_snapshots.list(\*\*params) -> SyncDiskSnapshotsCursorIDPage[DevboxSnapshotView] +- client.devboxes.disk_snapshots.delete(id) -> object +- client.devboxes.disk_snapshots.query_status(id) -> DevboxSnapshotAsyncStatusView + +## Browsers + +Types: + +```python +from runloop_api_client.types.devboxes import BrowserView +``` + +Methods: + +- client.devboxes.browsers.create(\*\*params) -> BrowserView +- client.devboxes.browsers.retrieve(id) -> BrowserView + +## Computers + +Types: + +```python +from runloop_api_client.types.devboxes import ( + ComputerView, + ComputerKeyboardInteractionResponse, + ComputerMouseInteractionResponse, + ComputerScreenInteractionResponse, +) +``` + +Methods: + +- client.devboxes.computers.create(\*\*params) -> ComputerView +- client.devboxes.computers.retrieve(id) -> ComputerView +- client.devboxes.computers.keyboard_interaction(id, \*\*params) -> ComputerKeyboardInteractionResponse +- client.devboxes.computers.mouse_interaction(id, \*\*params) -> ComputerMouseInteractionResponse +- client.devboxes.computers.screen_interaction(id, \*\*params) -> ComputerScreenInteractionResponse + +## Logs + +Types: + +```python +from runloop_api_client.types.devboxes import DevboxLogsListView +``` + +Methods: + +- client.devboxes.logs.list(id, \*\*params) -> DevboxLogsListView + +## Executions + +Types: + +```python +from runloop_api_client.types.devboxes import ExecutionUpdateChunk +``` + +Methods: + +- client.devboxes.executions.retrieve(execution_id, \*, devbox_id, \*\*params) -> DevboxAsyncExecutionDetailView +- client.devboxes.executions.execute_async(id, \*\*params) -> DevboxAsyncExecutionDetailView +- client.devboxes.executions.execute_sync(id, \*\*params) -> DevboxExecutionDetailView +- client.devboxes.executions.kill(execution_id, \*, devbox_id, \*\*params) -> DevboxAsyncExecutionDetailView +- client.devboxes.executions.send_std_in(execution_id, \*, devbox_id, \*\*params) -> DevboxSendStdInResult +- client.devboxes.executions.stream_stderr_updates(execution_id, \*, devbox_id, \*\*params) -> ExecutionUpdateChunk +- client.devboxes.executions.stream_stdout_updates(execution_id, \*, devbox_id, \*\*params) -> ExecutionUpdateChunk + +# Scenarios + +Types: + +```python +from runloop_api_client.types import ( + InputContext, + InputContextUpdate, + ScenarioCreateParameters, + ScenarioEnvironment, + ScenarioRunListView, + ScenarioRunView, + ScenarioUpdateParameters, + ScenarioView, + ScoringContract, + ScoringContractResultView, + ScoringContractUpdate, + ScoringFunction, + ScoringFunctionResultView, + StartScenarioRunParameters, +) +``` + +Methods: + +- client.scenarios.create(\*\*params) -> ScenarioView +- client.scenarios.retrieve(id) -> ScenarioView +- client.scenarios.update(id, \*\*params) -> ScenarioView +- client.scenarios.list(\*\*params) -> SyncScenariosCursorIDPage[ScenarioView] +- client.scenarios.archive(id) -> ScenarioView +- client.scenarios.list_public(\*\*params) -> SyncScenariosCursorIDPage[ScenarioView] +- client.scenarios.start_run(\*\*params) -> ScenarioRunView + +## Runs + +Methods: + +- client.scenarios.runs.retrieve(id) -> ScenarioRunView +- client.scenarios.runs.list(\*\*params) -> SyncBenchmarkRunsCursorIDPage[ScenarioRunView] +- client.scenarios.runs.cancel(id) -> ScenarioRunView +- client.scenarios.runs.complete(id) -> ScenarioRunView +- client.scenarios.runs.download_logs(id) -> BinaryAPIResponse +- client.scenarios.runs.score(id) -> ScenarioRunView + +## Scorers + +Types: + +```python +from runloop_api_client.types.scenarios import ( + ScorerCreateResponse, + ScorerRetrieveResponse, + ScorerUpdateResponse, + ScorerListResponse, +) +``` + +Methods: + +- client.scenarios.scorers.create(\*\*params) -> ScorerCreateResponse +- client.scenarios.scorers.retrieve(id) -> ScorerRetrieveResponse +- client.scenarios.scorers.update(id, \*\*params) -> ScorerUpdateResponse +- client.scenarios.scorers.list(\*\*params) -> SyncScenarioScorersCursorIDPage[ScorerListResponse] + +# Objects + +Types: + +```python +from runloop_api_client.types import ( + ObjectCreateParameters, + ObjectDownloadURLView, + ObjectListView, + ObjectView, +) +``` + +Methods: + +- client.objects.create(\*\*params) -> ObjectView +- client.objects.retrieve(id) -> ObjectView +- client.objects.list(\*\*params) -> SyncObjectsCursorIDPage[ObjectView] +- client.objects.delete(id) -> ObjectView +- client.objects.complete(id) -> ObjectView +- client.objects.download(id, \*\*params) -> ObjectDownloadURLView +- client.objects.list_public(\*\*params) -> SyncObjectsCursorIDPage[ObjectView] + +# Repositories + +Types: + +```python +from runloop_api_client.types import ( + RepositoryConnectionListView, + RepositoryConnectionView, + RepositoryInspectionDetails, + RepositoryInspectionListView, + RepositoryManifestView, +) +``` + +Methods: + +- client.repositories.create(\*\*params) -> RepositoryConnectionView +- client.repositories.retrieve(id) -> RepositoryConnectionView +- client.repositories.list(\*\*params) -> SyncRepositoriesCursorIDPage[RepositoryConnectionView] +- client.repositories.delete(id) -> object +- client.repositories.inspect(id, \*\*params) -> RepositoryInspectionDetails +- client.repositories.list_inspections(id) -> RepositoryInspectionListView +- client.repositories.refresh(id, \*\*params) -> object +- client.repositories.retrieve_inspection(id) -> RepositoryInspectionDetails + +# Secrets + +Types: + +```python +from runloop_api_client.types import ( + SecretCreateParameters, + SecretListView, + SecretUpdateParameters, + SecretView, +) +``` + +Methods: + +- client.secrets.create(\*\*params) -> SecretView +- client.secrets.update(name, \*\*params) -> SecretView +- client.secrets.list(\*\*params) -> SecretListView +- client.secrets.delete(name) -> SecretView + +# NetworkPolicies + +Types: + +```python +from runloop_api_client.types import ( + NetworkPolicyCreateParameters, + NetworkPolicyListView, + NetworkPolicyUpdateParameters, + NetworkPolicyView, +) +``` + +Methods: + +- client.network_policies.create(\*\*params) -> NetworkPolicyView +- client.network_policies.retrieve(id) -> NetworkPolicyView +- client.network_policies.update(id, \*\*params) -> NetworkPolicyView +- client.network_policies.list(\*\*params) -> SyncNetworkPoliciesCursorIDPage[NetworkPolicyView] +- client.network_policies.delete(id) -> NetworkPolicyView + +# GatewayConfigs + +Types: + +```python +from runloop_api_client.types import ( + GatewayConfigCreateParameters, + GatewayConfigListView, + GatewayConfigUpdateParameters, + GatewayConfigView, +) +``` + +Methods: + +- client.gateway_configs.create(\*\*params) -> GatewayConfigView +- client.gateway_configs.retrieve(id) -> GatewayConfigView +- client.gateway_configs.update(id, \*\*params) -> GatewayConfigView +- client.gateway_configs.list(\*\*params) -> SyncGatewayConfigsCursorIDPage[GatewayConfigView] +- client.gateway_configs.delete(id) -> GatewayConfigView + +# McpConfigs + +Types: + +```python +from runloop_api_client.types import ( + McpConfigCreateParameters, + McpConfigListView, + McpConfigUpdateParameters, + McpConfigView, +) +``` + +Methods: + +- client.mcp_configs.create(\*\*params) -> McpConfigView +- client.mcp_configs.retrieve(id) -> McpConfigView +- client.mcp_configs.update(id, \*\*params) -> McpConfigView +- client.mcp_configs.list(\*\*params) -> SyncMcpConfigsCursorIDPage[McpConfigView] +- client.mcp_configs.delete(id) -> McpConfigView diff --git a/bin/check-release-environment b/bin/check-release-environment new file mode 100644 index 000000000..b845b0f4c --- /dev/null +++ b/bin/check-release-environment @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +errors=() + +if [ -z "${PYPI_TOKEN}" ]; then + errors+=("The PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") +fi + +lenErrors=${#errors[@]} + +if [[ lenErrors -gt 0 ]]; then + echo -e "Found the following errors in the release environment:\n" + + for error in "${errors[@]}"; do + echo -e "- $error\n" + done + + exit 1 +fi + +echo "The environment is ready to push releases!" diff --git a/bin/publish-pypi b/bin/publish-pypi new file mode 100644 index 000000000..e72ca2fa4 --- /dev/null +++ b/bin/publish-pypi @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -eux +rm -rf dist +mkdir -p dist +uv build +uv publish --token=$PYPI_TOKEN diff --git a/examples/.keep b/examples/.keep new file mode 100644 index 000000000..d8c73e937 --- /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/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..01a86e12d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,255 @@ +[project] +name = "runloop_api_client" +version = "1.11.0" +description = "The official Python library for the runloop API" +dynamic = ["readme"] +license = "MIT" +authors = [ +{ name = "Runloop", email = "support@runloop.ai" }, +] + +dependencies = [ + "httpx>=0.23.0, <1", + "pydantic>=1.9.0, <3", + "typing-extensions>=4.10, <5", + "anyio>=3.5.0, <5", + "distro>=1.7.0, <2", + "sniffio", +] + +requires-python = ">= 3.9" +classifiers = [ + "Typing :: Typed", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Operating System :: OS Independent", + "Operating System :: POSIX", + "Operating System :: MacOS", + "Operating System :: POSIX :: Linux", + "Operating System :: Microsoft :: Windows", + "Topic :: Software Development :: Libraries :: Python Modules", + "License :: OSI Approved :: MIT License" +] + +[project.urls] +Homepage = "https://github.com/runloopai/api-client-python" +Repository = "https://github.com/runloopai/api-client-python" + +[project.optional-dependencies] +aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.9"] + +[tool.uv] +managed = true +required-version = ">=0.9" +conflicts = [ + [ + { group = "pydantic-v1" }, + { group = "pydantic-v2" }, + ], +] + +[dependency-groups] +# version pins are in uv.lock +dev = [ + "pyright==1.1.399", + "mypy==1.17", + "respx", + "pytest", + "pytest-asyncio", + "ruff", + "time-machine", + "dirty-equals>=0.6.0", + "importlib-metadata>=6.7.0", + "rich>=13.7.1", + "pytest-xdist>=3.6.1", +] +pydantic-v1 = [ + "pydantic>=1.9.0,<2", +] +pydantic-v2 = [ + "pydantic~=2.0 ; python_full_version < '3.14'", + "pydantic~=2.12 ; python_full_version >= '3.14'", +] + +[build-system] +requires = ["hatchling==1.26.3", "hatch-fancy-pypi-readme"] +build-backend = "hatchling.build" + +[tool.hatch.build] +include = [ + "src/*" +] + +[tool.hatch.build.targets.wheel] +packages = ["src/runloop_api_client"] + +[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/runloopai/api-client-python/tree/main/\g<2>)' + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = "--tb=short -n auto" +xfail_strict = true +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "session" +filterwarnings = [ + "error" +] + +[tool.pyright] +# this enables practically every flag given by pyright. +# there are a couple of flags that are still disabled by +# default in strict mode as they are experimental and niche. +typeCheckingMode = "strict" +pythonVersion = "3.9" + +exclude = [ + "_dev", + ".venv", + ".nox", + ".git", +] + +reportImplicitOverride = true +reportOverlappingOverload = false + +reportImportCycles = false +reportPrivateUsage = false + +[tool.mypy] +pretty = true +show_error_codes = true + +# Exclude _files.py because mypy isn't smart enough to apply +# the correct type narrowing and as this is an internal module +# it's fine to just use Pyright. +# +# We also exclude our `tests` as mypy doesn't always infer +# types correctly and Pyright will still catch any type errors. +exclude = ['src/runloop_api_client/_files.py', '_dev/.*.py', 'tests/.*'] + +strict_equality = true +implicit_reexport = true +check_untyped_defs = true +no_implicit_optional = true + +warn_return_any = true +warn_unreachable = true +warn_unused_configs = true + +# Turn these options off as it could cause conflicts +# with the Pyright options. +warn_unused_ignores = false +warn_redundant_casts = false + +disallow_any_generics = true +disallow_untyped_defs = true +disallow_untyped_calls = true +disallow_subclassing_any = true +disallow_incomplete_defs = true +disallow_untyped_decorators = true +cache_fine_grained = true + +# By default, mypy reports an error if you assign a value to the result +# of a function call that doesn't return anything. We do this in our test +# cases: +# ``` +# result = ... +# assert result is None +# ``` +# Changing this codegen to make mypy happy would increase complexity +# and would not be worth it. +disable_error_code = "func-returns-value,overload-cannot-match" + +# https://github.com/python/mypy/issues/12162 +[[tool.mypy.overrides]] +module = "black.files.*" +ignore_errors = true +ignore_missing_imports = true + + +[tool.ruff] +line-length = 120 +output-format = "grouped" +target-version = "py38" + +[tool.ruff.format] +docstring-code-format = true + +[tool.ruff.lint] +select = [ + # isort + "I", + # bugbear rules + "B", + # remove unused imports + "F401", + # check for missing future annotations + "FA102", + # bare except statements + "E722", + # unused arguments + "ARG", + # print statements + "T201", + "T203", + # misuse of typing.TYPE_CHECKING + "TC004", + # import rules + "TID251", +] +ignore = [ + # mutable defaults + "B006", +] +unfixable = [ + # disable auto fix for print statements + "T201", + "T203", +] + +extend-safe-fixes = ["FA102"] + +[tool.ruff.lint.flake8-tidy-imports.banned-api] +"functools.lru_cache".msg = "This function does not retain type information for the wrapped function's arguments; The `lru_cache` function from `_utils` should be used instead" + +[tool.ruff.lint.isort] +length-sort = true +length-sort-straight = true +combine-as-imports = true +extra-standard-library = ["typing_extensions"] +known-first-party = ["runloop_api_client", "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/release-please-config.json b/release-please-config.json new file mode 100644 index 000000000..dffe9e51f --- /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/runloop_api_client/_version.py" + ] +} \ No newline at end of file diff --git a/requirements-dev.lock b/requirements-dev.lock new file mode 100644 index 000000000..4def5ae9c --- /dev/null +++ b/requirements-dev.lock @@ -0,0 +1,110 @@ +# This file was autogenerated by uv via the following command: +# uv export -o requirements-dev.lock --no-hashes +-e . +annotated-types==0.7.0 + # via pydantic +anyio==4.12.1 + # via + # httpx + # runloop-api-client +backports-asyncio-runner==1.2.0 ; python_full_version < '3.11' + # via pytest-asyncio +certifi==2026.1.4 + # via + # httpcore + # httpx +colorama==0.4.6 ; sys_platform == 'win32' + # via pytest +dirty-equals==0.11 +distro==1.9.0 + # via runloop-api-client +exceptiongroup==1.3.1 ; python_full_version < '3.11' + # via + # anyio + # pytest +execnet==2.1.2 + # via pytest-xdist +h11==0.16.0 + # via httpcore +httpcore==1.0.9 + # via httpx +httpx==0.28.1 + # via + # respx + # runloop-api-client +idna==3.11 + # via + # anyio + # httpx +importlib-metadata==8.7.1 +iniconfig==2.1.0 ; python_full_version < '3.10' + # via pytest +iniconfig==2.3.0 ; python_full_version >= '3.10' + # via pytest +markdown-it-py==3.0.0 ; python_full_version < '3.10' + # via rich +markdown-it-py==4.0.0 ; python_full_version >= '3.10' + # via rich +mdurl==0.1.2 + # via markdown-it-py +mypy==1.17.0 +mypy-extensions==1.1.0 + # via mypy +nodeenv==1.10.0 + # via pyright +packaging==25.0 + # via pytest +pathspec==1.0.3 + # via mypy +pluggy==1.6.0 + # via pytest +pydantic==2.12.5 + # via runloop-api-client +pydantic-core==2.41.5 + # via pydantic +pygments==2.19.2 + # via + # pytest + # rich +pyright==1.1.399 +pytest==8.4.2 ; python_full_version < '3.10' + # via + # pytest-asyncio + # pytest-xdist +pytest==9.0.2 ; python_full_version >= '3.10' + # via + # pytest-asyncio + # pytest-xdist +pytest-asyncio==1.2.0 ; python_full_version < '3.10' +pytest-asyncio==1.3.0 ; python_full_version >= '3.10' +pytest-xdist==3.8.0 +python-dateutil==2.9.0.post0 ; python_full_version < '3.10' + # via time-machine +respx==0.22.0 +rich==14.2.0 +ruff==0.14.13 +six==1.17.0 ; python_full_version < '3.10' + # via python-dateutil +sniffio==1.3.1 + # via runloop-api-client +time-machine==2.19.0 ; python_full_version < '3.10' +time-machine==3.2.0 ; python_full_version >= '3.10' +tomli==2.4.0 ; python_full_version < '3.11' + # via + # mypy + # pytest +typing-extensions==4.15.0 + # via + # anyio + # exceptiongroup + # mypy + # pydantic + # pydantic-core + # pyright + # pytest-asyncio + # runloop-api-client + # typing-inspection +typing-inspection==0.4.2 + # via pydantic +zipp==3.23.0 + # via importlib-metadata diff --git a/scripts/bootstrap b/scripts/bootstrap new file mode 100755 index 000000000..4638ec694 --- /dev/null +++ b/scripts/bootstrap @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then + brew bundle check >/dev/null 2>&1 || { + echo -n "==> Install Homebrew dependencies? (y/N): " + read -r response + case "$response" in + [yY][eE][sS]|[yY]) + brew bundle + ;; + *) + ;; + esac + echo + } +fi + +echo "==> Installing Python…" +uv python install + +echo "==> Installing Python dependencies…" +uv sync --all-extras + +echo "==> Exporting Python dependencies…" +# note: `--no-hashes` is required because of https://github.com/pypa/pip/issues/4995 +uv export -o requirements-dev.lock --no-hashes diff --git a/scripts/format b/scripts/format new file mode 100755 index 000000000..c8e1f69d2 --- /dev/null +++ b/scripts/format @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +echo "==> Running ruff" +uv run ruff format +uv run ruff check --fix . +# run formatting again to fix any inconsistencies when imports are stripped +uv run ruff format + +echo "==> Formatting docs" +uv run python scripts/utils/ruffen-docs.py README.md $(find . -type f -name api.md) diff --git a/scripts/lint b/scripts/lint new file mode 100755 index 000000000..715dbe325 --- /dev/null +++ b/scripts/lint @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +if [ "$1" = "--fix" ]; then + echo "==> Running ruff with --fix" + uv run ruff check . --fix +else + echo "==> Running ruff" + uv run ruff check . +fi + +echo "==> Running pyright" +uv run pyright + +echo "==> Running mypy" +uv run mypy . + +echo "==> Making sure it imports" +uv run python -c 'import runloop_api_client' diff --git a/scripts/mock b/scripts/mock new file mode 100755 index 000000000..bcf3b392b --- /dev/null +++ b/scripts/mock @@ -0,0 +1,52 @@ +#!/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 + # Pre-install the package so the download doesn't eat into the startup timeout + npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism --version + + npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & + + # Wait for server to come online (max 30s) + echo -n "Waiting for server" + attempts=0 + while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do + attempts=$((attempts + 1)) + if [ "$attempts" -ge 300 ]; then + echo + echo "Timed out waiting for Prism server to start" + cat .prism.log + exit 1 + fi + echo -n "." + sleep 0.1 + done + + if grep -q "✖ fatal" ".prism.log"; then + cat .prism.log + exit 1 + fi + + echo +else + npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" +fi diff --git a/scripts/test b/scripts/test new file mode 100755 index 000000000..b56970b78 --- /dev/null +++ b/scripts/test @@ -0,0 +1,84 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' # No Color + +function prism_is_running() { + curl --silent "http://localhost:4010" >/dev/null 2>&1 +} + +kill_server_on_port() { + pids=$(lsof -t -i tcp:"$1" || echo "") + if [ "$pids" != "" ]; then + kill "$pids" + echo "Stopped $pids." + fi +} + +function is_overriding_api_base_url() { + [ -n "$TEST_API_BASE_URL" ] +} + +if ! is_overriding_api_base_url && ! prism_is_running ; then + # When we exit this script, make sure to kill the background mock server process + trap 'kill_server_on_port 4010' EXIT + + # Start the dev server + ./scripts/mock --daemon +fi + +if is_overriding_api_base_url ; then + echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}" + echo +elif ! prism_is_running ; then + echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server" + echo -e "running against your OpenAPI spec." + echo + echo -e "To run the server, pass in the path or url of your OpenAPI" + echo -e "spec to the prism command:" + echo + echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}" + echo + + exit 1 +else + echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}" + echo +fi + +export DEFER_PYDANTIC_BUILD=false + +# Note that we need to specify the patch version here so that uv +# won't use unstable (alpha, beta, rc) releases for the tests +PY_VERSION_MIN=">=3.9.0" +PY_VERSION_MAX=">=3.14.0" + +function run_tests() { + echo "==> Running tests with Pydantic v2" + uv run --isolated --all-extras pytest "$@" + + # Skip Pydantic v1 tests on latest Python (not supported) + if [[ "$UV_PYTHON" != "$PY_VERSION_MAX" ]]; then + echo "==> Running tests with Pydantic v1" + uv run --isolated --all-extras --group=pydantic-v1 pytest "$@" + fi +} + +# If UV_PYTHON is already set in the environment, just run the command once +if [[ -n "$UV_PYTHON" ]]; then + run_tests "$@" +else + # If UV_PYTHON is not set, run the command for min and max versions + + echo "==> Running tests for Python $PY_VERSION_MIN" + UV_PYTHON="$PY_VERSION_MIN" run_tests "$@" + + echo "==> Running tests for Python $PY_VERSION_MAX" + UV_PYTHON="$PY_VERSION_MAX" run_tests "$@" +fi diff --git a/scripts/utils/ruffen-docs.py b/scripts/utils/ruffen-docs.py new file mode 100644 index 000000000..0cf2bd2fd --- /dev/null +++ b/scripts/utils/ruffen-docs.py @@ -0,0 +1,167 @@ +# fork of https://github.com/asottile/blacken-docs adapted for ruff +from __future__ import annotations + +import re +import sys +import argparse +import textwrap +import contextlib +import subprocess +from typing import Match, Optional, Sequence, Generator, NamedTuple, cast + +MD_RE = re.compile( + r"(?P^(?P *)```\s*python\n)" r"(?P.*?)" r"(?P^(?P=indent)```\s*$)", + re.DOTALL | re.MULTILINE, +) +MD_PYCON_RE = re.compile( + r"(?P^(?P *)```\s*pycon\n)" r"(?P.*?)" r"(?P^(?P=indent)```.*$)", + re.DOTALL | re.MULTILINE, +) +PYCON_PREFIX = ">>> " +PYCON_CONTINUATION_PREFIX = "..." +PYCON_CONTINUATION_RE = re.compile( + rf"^{re.escape(PYCON_CONTINUATION_PREFIX)}( |$)", +) +DEFAULT_LINE_LENGTH = 100 + + +class CodeBlockError(NamedTuple): + offset: int + exc: Exception + + +def format_str( + src: str, +) -> tuple[str, Sequence[CodeBlockError]]: + errors: list[CodeBlockError] = [] + + @contextlib.contextmanager + def _collect_error(match: Match[str]) -> Generator[None, None, None]: + try: + yield + except Exception as e: + errors.append(CodeBlockError(match.start(), e)) + + def _md_match(match: Match[str]) -> str: + code = textwrap.dedent(match["code"]) + with _collect_error(match): + code = format_code_block(code) + code = textwrap.indent(code, match["indent"]) + return f"{match['before']}{code}{match['after']}" + + def _pycon_match(match: Match[str]) -> str: + code = "" + fragment = cast(Optional[str], None) + + def finish_fragment() -> None: + nonlocal code + nonlocal fragment + + if fragment is not None: + with _collect_error(match): + fragment = format_code_block(fragment) + fragment_lines = fragment.splitlines() + code += f"{PYCON_PREFIX}{fragment_lines[0]}\n" + for line in fragment_lines[1:]: + # Skip blank lines to handle Black adding a blank above + # functions within blocks. A blank line would end the REPL + # continuation prompt. + # + # >>> if True: + # ... def f(): + # ... pass + # ... + if line: + code += f"{PYCON_CONTINUATION_PREFIX} {line}\n" + if fragment_lines[-1].startswith(" "): + code += f"{PYCON_CONTINUATION_PREFIX}\n" + fragment = None + + indentation = None + for line in match["code"].splitlines(): + orig_line, line = line, line.lstrip() + if indentation is None and line: + indentation = len(orig_line) - len(line) + continuation_match = PYCON_CONTINUATION_RE.match(line) + if continuation_match and fragment is not None: + fragment += line[continuation_match.end() :] + "\n" + else: + finish_fragment() + if line.startswith(PYCON_PREFIX): + fragment = line[len(PYCON_PREFIX) :] + "\n" + else: + code += orig_line[indentation:] + "\n" + finish_fragment() + return code + + def _md_pycon_match(match: Match[str]) -> str: + code = _pycon_match(match) + code = textwrap.indent(code, match["indent"]) + return f"{match['before']}{code}{match['after']}" + + src = MD_RE.sub(_md_match, src) + src = MD_PYCON_RE.sub(_md_pycon_match, src) + return src, errors + + +def format_code_block(code: str) -> str: + return subprocess.check_output( + [ + sys.executable, + "-m", + "ruff", + "format", + "--stdin-filename=script.py", + f"--line-length={DEFAULT_LINE_LENGTH}", + ], + encoding="utf-8", + input=code, + ) + + +def format_file( + filename: str, + skip_errors: bool, +) -> int: + with open(filename, encoding="UTF-8") as f: + contents = f.read() + new_contents, errors = format_str(contents) + for error in errors: + lineno = contents[: error.offset].count("\n") + 1 + print(f"{filename}:{lineno}: code block parse error {error.exc}") + if errors and not skip_errors: + return 1 + if contents != new_contents: + print(f"{filename}: Rewriting...") + with open(filename, "w", encoding="UTF-8") as f: + f.write(new_contents) + return 0 + else: + return 0 + + +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser() + parser.add_argument( + "-l", + "--line-length", + type=int, + default=DEFAULT_LINE_LENGTH, + ) + parser.add_argument( + "-S", + "--skip-string-normalization", + action="store_true", + ) + parser.add_argument("-E", "--skip-errors", action="store_true") + parser.add_argument("filenames", nargs="*") + args = parser.parse_args(argv) + + retv = 0 + for filename in args.filenames: + retv |= format_file(filename, skip_errors=args.skip_errors) + return retv + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh new file mode 100755 index 000000000..36ac739d1 --- /dev/null +++ b/scripts/utils/upload-artifact.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -exuo pipefail + +FILENAME=$(basename dist/*.whl) + +RESPONSE=$(curl -X POST "$URL?filename=$FILENAME" \ + -H "Authorization: Bearer $AUTH" \ + -H "Content-Type: application/json") + +SIGNED_URL=$(echo "$RESPONSE" | jq -r '.url') + +if [[ "$SIGNED_URL" == "null" ]]; then + echo -e "\033[31mFailed to get signed URL.\033[0m" + exit 1 +fi + +UPLOAD_RESPONSE=$(curl -v -X PUT \ + -H "Content-Type: binary/octet-stream" \ + --data-binary "@dist/$FILENAME" "$SIGNED_URL" 2>&1) + +if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then + echo -e "\033[32mUploaded build to Stainless storage.\033[0m" + echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/runloop-python/$SHA/$FILENAME'\033[0m" +else + echo -e "\033[31mFailed to upload artifact.\033[0m" + exit 1 +fi diff --git a/src/runloop/lib/.keep b/src/runloop/lib/.keep new file mode 100644 index 000000000..5e2c99fdb --- /dev/null +++ b/src/runloop/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/runloop_api_client/__init__.py b/src/runloop_api_client/__init__.py new file mode 100644 index 000000000..d6be57db2 --- /dev/null +++ b/src/runloop_api_client/__init__.py @@ -0,0 +1,92 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import typing as _t + +from . import types +from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes, omit, not_given +from ._utils import file_from_path +from ._client import Client, Stream, Runloop, Timeout, Transport, AsyncClient, AsyncStream, AsyncRunloop, 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, + RunloopError, + ConflictError, + NotFoundError, + APIStatusError, + RateLimitError, + APITimeoutError, + BadRequestError, + APIConnectionError, + AuthenticationError, + InternalServerError, + PermissionDeniedError, + UnprocessableEntityError, + APIResponseValidationError, +) +from ._base_client import DefaultHttpxClient, DefaultAioHttpClient, DefaultAsyncHttpxClient +from ._utils._logs import setup_logging as _setup_logging + +__all__ = [ + "types", + "__version__", + "__title__", + "NoneType", + "Transport", + "ProxiesTypes", + "NotGiven", + "NOT_GIVEN", + "not_given", + "Omit", + "omit", + "RunloopError", + "APIError", + "APIStatusError", + "APITimeoutError", + "APIConnectionError", + "APIResponseValidationError", + "BadRequestError", + "AuthenticationError", + "PermissionDeniedError", + "NotFoundError", + "ConflictError", + "UnprocessableEntityError", + "RateLimitError", + "InternalServerError", + "Timeout", + "RequestOptions", + "Client", + "AsyncClient", + "Stream", + "AsyncStream", + "Runloop", + "AsyncRunloop", + "file_from_path", + "BaseModel", + "DEFAULT_TIMEOUT", + "DEFAULT_MAX_RETRIES", + "DEFAULT_CONNECTION_LIMITS", + "DefaultHttpxClient", + "DefaultAsyncHttpxClient", + "DefaultAioHttpClient", +] + +if not _t.TYPE_CHECKING: + from ._utils._resources_proxy import resources as resources + +_setup_logging() + +# Update the __module__ attribute for exported symbols so that +# error messages point to this module instead of the module +# it was originally defined in, e.g. +# runloop_api_client._exceptions.NotFoundError -> runloop_api_client.NotFoundError +__locals = locals() +for __name in __all__: + if not __name.startswith("__"): + try: + __locals[__name].__module__ = "runloop_api_client" + except (TypeError, AttributeError): + # Some of our exported symbols are builtins which we can't set attributes for. + pass diff --git a/src/runloop_api_client/_base_client.py b/src/runloop_api_client/_base_client.py new file mode 100644 index 000000000..8428705b5 --- /dev/null +++ b/src/runloop_api_client/_base_client.py @@ -0,0 +1,2127 @@ +from __future__ import annotations + +import sys +import json +import time +import uuid +import email +import asyncio +import inspect +import logging +import platform +import warnings +import email.utils +from types import TracebackType +from random import random +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Type, + Union, + Generic, + Mapping, + TypeVar, + Iterable, + Iterator, + Optional, + Generator, + AsyncIterator, + cast, + overload, +) +from typing_extensions import Literal, override, get_origin + +import anyio +import httpx +import distro +import pydantic +from httpx import URL +from pydantic import PrivateAttr + +from . import _exceptions +from ._qs import Querystring +from ._files import to_httpx_files, async_to_httpx_files +from ._types import ( + Body, + Omit, + Query, + Headers, + Timeout, + NotGiven, + ResponseT, + AnyMapping, + PostParser, + BinaryTypes, + RequestFiles, + HttpxSendArgs, + RequestOptions, + AsyncBinaryTypes, + HttpxRequestFiles, + ModelBuilderProtocol, + not_given, +) +from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping +from ._compat import PYDANTIC_V1, model_copy, model_dump +from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type +from ._response import ( + APIResponse, + BaseAPIResponse, + AsyncAPIResponse, + extract_response_type, +) +from ._constants import ( + DEFAULT_TIMEOUT, + MAX_RETRY_DELAY, + DEFAULT_MAX_RETRIES, + INITIAL_RETRY_DELAY, + RAW_RESPONSE_HEADER, + OVERRIDE_CAST_TO_HEADER, + DEFAULT_CONNECTION_LIMITS, +) +from ._streaming import Stream, SSEDecoder, AsyncStream, SSEBytesDecoder +from ._exceptions import ( + APIStatusError, + APITimeoutError, + APIConnectionError, + APIResponseValidationError, +) +from ._utils._json import openapi_dumps + +log: logging.Logger = logging.getLogger(__name__) + +# TODO: make base page type vars covariant +SyncPageT = TypeVar("SyncPageT", bound="BaseSyncPage[Any]") +AsyncPageT = TypeVar("AsyncPageT", bound="BaseAsyncPage[Any]") + + +_T = TypeVar("_T") +_T_co = TypeVar("_T_co", covariant=True) + +_StreamT = TypeVar("_StreamT", bound=Stream[Any]) +_AsyncStreamT = TypeVar("_AsyncStreamT", bound=AsyncStream[Any]) + +if TYPE_CHECKING: + from httpx._config import ( + DEFAULT_TIMEOUT_CONFIG, # pyright: ignore[reportPrivateImportUsage] + ) + + HTTPX_DEFAULT_TIMEOUT = DEFAULT_TIMEOUT_CONFIG +else: + try: + from httpx._config import DEFAULT_TIMEOUT_CONFIG as HTTPX_DEFAULT_TIMEOUT + except ImportError: + # taken from https://github.com/encode/httpx/blob/3ba5fe0d7ac70222590e759c31442b1cab263791/httpx/_config.py#L366 + HTTPX_DEFAULT_TIMEOUT = Timeout(5.0) + + +class PageInfo: + """Stores the necessary information to build the request to retrieve the next page. + + Either `url` or `params` must be set. + """ + + url: URL | NotGiven + params: Query | NotGiven + json: Body | NotGiven + + @overload + def __init__( + self, + *, + url: URL, + ) -> None: ... + + @overload + def __init__( + self, + *, + params: Query, + ) -> None: ... + + @overload + def __init__( + self, + *, + json: Body, + ) -> None: ... + + def __init__( + self, + *, + url: URL | NotGiven = not_given, + json: Body | NotGiven = not_given, + params: Query | NotGiven = not_given, + ) -> None: + self.url = url + self.json = json + self.params = params + + @override + def __repr__(self) -> str: + if self.url: + return f"{self.__class__.__name__}(url={self.url})" + if self.json: + return f"{self.__class__.__name__}(json={self.json})" + return f"{self.__class__.__name__}(params={self.params})" + + +class BasePage(GenericModel, Generic[_T]): + """ + Defines the core interface for pagination. + + Type Args: + ModelT: The pydantic model that represents an item in the response. + + Methods: + has_next_page(): Check if there is another page available + next_page_info(): Get the necessary information to make a request for the next page + """ + + _options: FinalRequestOptions = PrivateAttr() + _model: Type[_T] = PrivateAttr() + + def has_next_page(self) -> bool: + items = self._get_page_items() + if not items: + return False + return self.next_page_info() is not None + + def next_page_info(self) -> Optional[PageInfo]: ... + + def _get_page_items(self) -> Iterable[_T]: # type: ignore[empty-body] + ... + + def _params_from_url(self, url: URL) -> httpx.QueryParams: + # TODO: do we have to preprocess params here? + return httpx.QueryParams(cast(Any, self._options.params)).merge(url.params) + + def _info_to_options(self, info: PageInfo) -> FinalRequestOptions: + options = model_copy(self._options) + options._strip_raw_response_header() + + if not isinstance(info.params, NotGiven): + options.params = {**options.params, **info.params} + return options + + if not isinstance(info.url, NotGiven): + params = self._params_from_url(info.url) + url = info.url.copy_with(params=params) + options.params = dict(url.params) + options.url = str(url) + return options + + if not isinstance(info.json, NotGiven): + if not is_mapping(info.json): + raise TypeError("Pagination is only supported with mappings") + + if not options.json_data: + options.json_data = {**info.json} + else: + if not is_mapping(options.json_data): + raise TypeError("Pagination is only supported with mappings") + + options.json_data = {**options.json_data, **info.json} + return options + + raise ValueError("Unexpected PageInfo state") + + +class BaseSyncPage(BasePage[_T], Generic[_T]): + _client: SyncAPIClient = pydantic.PrivateAttr() + + def _set_private_attributes( + self, + client: SyncAPIClient, + model: Type[_T], + options: FinalRequestOptions, + ) -> None: + if (not PYDANTIC_V1) and getattr(self, "__pydantic_private__", None) is None: + self.__pydantic_private__ = {} + + self._model = model + self._client = client + self._options = options + + # Pydantic uses a custom `__iter__` method to support casting BaseModels + # to dictionaries. e.g. dict(model). + # As we want to support `for item in page`, this is inherently incompatible + # with the default pydantic behaviour. It is not possible to support both + # use cases at once. Fortunately, this is not a big deal as all other pydantic + # methods should continue to work as expected as there is an alternative method + # to cast a model to a dictionary, model.dict(), which is used internally + # by pydantic. + def __iter__(self) -> Iterator[_T]: # type: ignore + for page in self.iter_pages(): + for item in page._get_page_items(): + yield item + + def iter_pages(self: SyncPageT) -> Iterator[SyncPageT]: + page = self + while True: + yield page + if page.has_next_page(): + page = page.get_next_page() + else: + return + + def get_next_page(self: SyncPageT) -> SyncPageT: + info = self.next_page_info() + if not info: + raise RuntimeError( + "No next page expected; please check `.has_next_page()` before calling `.get_next_page()`." + ) + + options = self._info_to_options(info) + return self._client._request_api_list(self._model, page=self.__class__, options=options) + + +class AsyncPaginator(Generic[_T, AsyncPageT]): + def __init__( + self, + client: AsyncAPIClient, + options: FinalRequestOptions, + page_cls: Type[AsyncPageT], + model: Type[_T], + ) -> None: + self._model = model + self._client = client + self._options = options + self._page_cls = page_cls + + def __await__(self) -> Generator[Any, None, AsyncPageT]: + return self._get_page().__await__() + + async def _get_page(self) -> AsyncPageT: + def _parser(resp: AsyncPageT) -> AsyncPageT: + resp._set_private_attributes( + model=self._model, + options=self._options, + client=self._client, + ) + return resp + + self._options.post_parser = _parser + + return await self._client.request(self._page_cls, self._options) + + async def __aiter__(self) -> AsyncIterator[_T]: + # https://github.com/microsoft/pyright/issues/3464 + page = cast( + AsyncPageT, + await self, # type: ignore + ) + async for item in page: + yield item + + +class BaseAsyncPage(BasePage[_T], Generic[_T]): + _client: AsyncAPIClient = pydantic.PrivateAttr() + + def _set_private_attributes( + self, + model: Type[_T], + client: AsyncAPIClient, + options: FinalRequestOptions, + ) -> None: + if (not PYDANTIC_V1) and getattr(self, "__pydantic_private__", None) is None: + self.__pydantic_private__ = {} + + self._model = model + self._client = client + self._options = options + + async def __aiter__(self) -> AsyncIterator[_T]: + async for page in self.iter_pages(): + for item in page._get_page_items(): + yield item + + async def iter_pages(self: AsyncPageT) -> AsyncIterator[AsyncPageT]: + page = self + while True: + yield page + if page.has_next_page(): + page = await page.get_next_page() + else: + return + + async def get_next_page(self: AsyncPageT) -> AsyncPageT: + info = self.next_page_info() + if not info: + raise RuntimeError( + "No next page expected; please check `.has_next_page()` before calling `.get_next_page()`." + ) + + options = self._info_to_options(info) + return await self._client._request_api_list(self._model, page=self.__class__, options=options) + + +_HttpxClientT = TypeVar("_HttpxClientT", bound=Union[httpx.Client, httpx.AsyncClient]) +_DefaultStreamT = TypeVar("_DefaultStreamT", bound=Union[Stream[Any], AsyncStream[Any]]) + + +class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]): + _client: _HttpxClientT + _version: str + _base_url: URL + max_retries: int + timeout: Union[float, Timeout, None] + _strict_response_validation: bool + _idempotency_header: str | None + _default_stream_cls: type[_DefaultStreamT] | None = None + + def __init__( + self, + *, + version: str, + base_url: str | URL, + _strict_response_validation: bool, + max_retries: int = DEFAULT_MAX_RETRIES, + timeout: float | Timeout | None = DEFAULT_TIMEOUT, + custom_headers: Mapping[str, str] | None = None, + custom_query: Mapping[str, object] | None = None, + ) -> None: + self._version = version + self._base_url = self._enforce_trailing_slash(URL(base_url)) + self.max_retries = max_retries + self.timeout = timeout + self._custom_headers = custom_headers or {} + self._custom_query = custom_query or {} + self._strict_response_validation = _strict_response_validation + self._idempotency_header = None + self._platform: Platform | None = None + + if max_retries is None: # pyright: ignore[reportUnnecessaryComparison] + raise TypeError( + "max_retries cannot be None. If you want to disable retries, pass `0`; if you want unlimited retries, pass `math.inf` or a very high number; if you want the default behavior, pass `runloop_api_client.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, + # Pydantic v1 can't dump every type we support in content, so we exclude it for now. + exclude={ + "content", + } + if PYDANTIC_V1 + else {}, + ), + ) + kwargs: dict[str, Any] = {} + + json_data = options.json_data + if options.extra_json is not None: + if json_data is None: + json_data = cast(Body, options.extra_json) + elif is_mapping(json_data): + json_data = _merge_mappings(json_data, options.extra_json) + else: + raise RuntimeError(f"Unexpected JSON data type, {type(json_data)}, cannot merge with `extra_body`") + + headers = self._build_headers(options, retries_taken=retries_taken) + params = _merge_mappings(self.default_query, options.params) + content_type = headers.get("Content-Type") + files = options.files + + # If the given Content-Type header is multipart/form-data then it + # has to be removed so that httpx can generate the header with + # additional information for us as it has to be in this form + # for the server to be able to correctly parse the request: + # multipart/form-data; boundary=---abc-- + if content_type is not None and content_type.startswith("multipart/form-data"): + if "boundary" not in content_type: + # only remove the header if the boundary hasn't been explicitly set + # as the caller doesn't want httpx to come up with their own boundary + headers.pop("Content-Type") + + # As we are now sending multipart/form-data instead of application/json + # we need to tell httpx to use it, https://www.python-httpx.org/advanced/clients/#multipart-file-encoding + if json_data: + if not is_dict(json_data): + raise TypeError( + f"Expected query input to be a dictionary for multipart requests but got {type(json_data)} instead." + ) + kwargs["data"] = self._serialize_multipartform(json_data) + + # httpx determines whether or not to send a "multipart/form-data" + # request based on the truthiness of the "files" argument. + # This gets around that issue by generating a dict value that + # evaluates to true. + # + # https://github.com/encode/httpx/discussions/2399#discussioncomment-3814186 + if not files: + files = cast(HttpxRequestFiles, ForceMultipartDict()) + + prepared_url = self._prepare_url(options.url) + if "_" in prepared_url.host: + # work around https://github.com/encode/httpx/discussions/2880 + kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")} + + is_body_allowed = options.method.lower() != "get" + + if is_body_allowed: + if options.content is not None and json_data is not None: + raise TypeError("Passing both `content` and `json_data` is not supported") + if options.content is not None and files is not None: + raise TypeError("Passing both `content` and `files` is not supported") + if options.content is not None: + kwargs["content"] = options.content + elif isinstance(json_data, bytes): + kwargs["content"] = json_data + elif not files: + # Don't set content when JSON is sent as multipart/form-data, + # since httpx's content param overrides other body arguments + kwargs["content"] = openapi_dumps(json_data) if is_given(json_data) and json_data is not None else None + kwargs["files"] = files + else: + headers.pop("Content-Type", None) + kwargs.pop("data", None) + + # TODO: report this error to httpx + return self._client.build_request( # pyright: ignore[reportUnknownMemberType] + headers=headers, + timeout=self.timeout if isinstance(options.timeout, NotGiven) else options.timeout, + method=options.method, + url=prepared_url, + # the `Query` type that we use is incompatible with qs' + # `Params` type as it needs to be typed as `Mapping[str, object]` + # so that passing a `TypedDict` doesn't cause an error. + # https://github.com/microsoft/pyright/issues/3526#event-6715453066 + params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None, + **kwargs, + ) + + def _serialize_multipartform(self, data: Mapping[object, object]) -> dict[str, object]: + items = self.qs.stringify_items( + # TODO: type ignore is required as stringify_items is well typed but we can't be + # well typed without heavy validation. + data, # type: ignore + array_format="brackets", + ) + serialized: dict[str, object] = {} + for key, value in items: + existing = serialized.get(key) + + if not existing: + serialized[key] = value + continue + + # If a value has already been set for this key then that + # means we're sending data like `array[]=[1, 2, 3]` and we + # need to tell httpx that we want to send multiple values with + # the same key which is done by using a list or a tuple. + # + # Note: 2d arrays should never result in the same key at both + # levels so it's safe to assume that if the value is a list, + # it was because we changed it to be a list. + if is_list(existing): + existing.append(value) + else: + serialized[key] = [existing, value] + + return serialized + + def _maybe_override_cast_to(self, cast_to: type[ResponseT], options: FinalRequestOptions) -> type[ResponseT]: + if not is_given(options.headers): + return cast_to + + # make a copy of the headers so we don't mutate user-input + headers = dict(options.headers) + + # we internally support defining a temporary header to override the + # default `cast_to` type for use with `.with_raw_response` and `.with_streaming_response` + # see _response.py for implementation details + override_cast_to = headers.pop(OVERRIDE_CAST_TO_HEADER, not_given) + if is_given(override_cast_to): + options.headers = headers + return cast(Type[ResponseT], override_cast_to) + + return cast_to + + def _should_stream_response_body(self, request: httpx.Request) -> bool: + return request.headers.get(RAW_RESPONSE_HEADER) == "stream" # type: ignore[no-any-return] + + def _process_response_data( + self, + *, + data: object, + cast_to: type[ResponseT], + response: httpx.Response, + ) -> ResponseT: + if data is None: + return cast(ResponseT, None) + + if cast_to is object: + return cast(ResponseT, data) + + try: + if inspect.isclass(cast_to) and issubclass(cast_to, ModelBuilderProtocol): + return cast(ResponseT, cast_to.build(response=response, data=data)) + + if self._strict_response_validation: + return cast(ResponseT, validate_type(type_=cast_to, value=data)) + + return cast(ResponseT, construct_type(type_=cast_to, value=data)) + except pydantic.ValidationError as err: + raise APIResponseValidationError(response=response, body=data) from err + + @property + def qs(self) -> Querystring: + return Querystring() + + @property + def custom_auth(self) -> httpx.Auth | None: + return None + + @property + def auth_headers(self) -> dict[str, str]: + return {} + + @property + def default_headers(self) -> dict[str, str | Omit]: + return { + "Accept": "application/json", + "Content-Type": "application/json", + "User-Agent": self.user_agent, + **self.platform_headers(), + **self.auth_headers, + **self._custom_headers, + } + + @property + def default_query(self) -> dict[str, object]: + return { + **self._custom_query, + } + + def _validate_headers( + self, + headers: Headers, # noqa: ARG002 + custom_headers: Headers, # noqa: ARG002 + ) -> None: + """Validate the given default headers and custom headers. + + Does nothing by default. + """ + return + + @property + def user_agent(self) -> str: + return f"{self.__class__.__name__}/Python {self._version}" + + @property + def base_url(self) -> URL: + return self._base_url + + @base_url.setter + def base_url(self, url: URL | str) -> None: + self._base_url = self._enforce_trailing_slash(url if isinstance(url, URL) else URL(url)) + + def platform_headers(self) -> Dict[str, str]: + # the actual implementation is in a separate `lru_cache` decorated + # function because adding `lru_cache` to methods will leak memory + # https://github.com/python/cpython/issues/88476 + return platform_headers(self._version, platform=self._platform) + + def _parse_retry_after_header(self, response_headers: Optional[httpx.Headers] = None) -> float | None: + """Returns a float of the number of seconds (not milliseconds) to wait after retrying, or None if unspecified. + + About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After + See also https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After#syntax + """ + if response_headers is None: + return None + + # First, try the non-standard `retry-after-ms` header for milliseconds, + # which is more precise than integer-seconds `retry-after` + try: + retry_ms_header = response_headers.get("retry-after-ms", None) + return float(retry_ms_header) / 1000 + except (TypeError, ValueError): + pass + + # Next, try parsing `retry-after` header as seconds (allowing nonstandard floats). + retry_header = response_headers.get("retry-after") + try: + # note: the spec indicates that this should only ever be an integer + # but if someone sends a float there's no reason for us to not respect it + return float(retry_header) + except (TypeError, ValueError): + pass + + # Last, try parsing `retry-after` as a date. + retry_date_tuple = email.utils.parsedate_tz(retry_header) + if retry_date_tuple is None: + return None + + retry_date = email.utils.mktime_tz(retry_date_tuple) + return float(retry_date - time.time()) + + def _calculate_retry_timeout( + self, + remaining_retries: int, + options: FinalRequestOptions, + response_headers: Optional[httpx.Headers] = None, + ) -> float: + max_retries = options.get_max_retries(self.max_retries) + + # If the API asks us to wait a certain amount of time (and it's a reasonable amount), just do what it says. + retry_after = self._parse_retry_after_header(response_headers) + if retry_after is not None and 0 < retry_after <= 60: + return retry_after + + # Also cap retry count to 1000 to avoid any potential overflows with `pow` + nb_retries = min(max_retries - remaining_retries, 1000) + + # Apply exponential backoff, but not more than the max. + sleep_seconds = min(INITIAL_RETRY_DELAY * pow(2.0, nb_retries), MAX_RETRY_DELAY) + + # Apply some jitter, plus-or-minus half a second. + jitter = 1 - 0.25 * random() + timeout = sleep_seconds * jitter + return timeout if timeout >= 0 else 0 + + def _should_retry(self, response: httpx.Response) -> bool: + # Note: this is not a standard header + should_retry_header = response.headers.get("x-should-retry") + + # If the server explicitly says whether or not to retry, obey. + if should_retry_header == "true": + log.debug("Retrying as header `x-should-retry` is set to `true`") + return True + if should_retry_header == "false": + log.debug("Not retrying as header `x-should-retry` is set to `false`") + return False + + # Retry on request timeouts. + if response.status_code == 408: + log.debug("Retrying due to status code %i", response.status_code) + return True + + # Retry on lock timeouts. + if response.status_code == 409: + log.debug("Retrying due to status code %i", response.status_code) + return True + + # Retry on rate limits. + if response.status_code == 429: + log.debug("Retrying due to status code %i", response.status_code) + return True + + # Retry internal errors. + if response.status_code >= 500: + log.debug("Retrying due to status code %i", response.status_code) + return True + + log.debug("Not retrying") + return False + + def _idempotency_key(self) -> str: + return f"stainless-python-retry-{uuid.uuid4()}" + + +class _DefaultHttpxClient(httpx.Client): + def __init__(self, **kwargs: Any) -> None: + kwargs.setdefault("timeout", DEFAULT_TIMEOUT) + kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) + kwargs.setdefault("follow_redirects", True) + super().__init__(**kwargs) + + +if TYPE_CHECKING: + DefaultHttpxClient = httpx.Client + """An alias to `httpx.Client` that provides the same defaults that this SDK + uses internally. + + This is useful because overriding the `http_client` with your own instance of + `httpx.Client` will result in httpx's defaults being used, not ours. + """ +else: + DefaultHttpxClient = _DefaultHttpxClient + + +class SyncHttpxClientWrapper(DefaultHttpxClient): + def __del__(self) -> None: + if self.is_closed: + return + + try: + self.close() + except Exception: + pass + + +class SyncAPIClient(BaseClient[httpx.Client, Stream[Any]]): + _client: httpx.Client + _default_stream_cls: type[Stream[Any]] | None = None + + def __init__( + self, + *, + version: str, + base_url: str | URL, + max_retries: int = DEFAULT_MAX_RETRIES, + timeout: float | Timeout | None | NotGiven = not_given, + http_client: httpx.Client | None = None, + custom_headers: Mapping[str, str] | None = None, + custom_query: Mapping[str, object] | None = None, + _strict_response_validation: bool, + ) -> None: + if not is_given(timeout): + # if the user passed in a custom http client with a non-default + # timeout set then we use that timeout. + # + # note: there is an edge case here where the user passes in a client + # where they've explicitly set the timeout to match the default timeout + # as this check is structural, meaning that we'll think they didn't + # pass in a timeout and will ignore it + if http_client and http_client.timeout != HTTPX_DEFAULT_TIMEOUT: + timeout = http_client.timeout + else: + timeout = DEFAULT_TIMEOUT + + if http_client is not None and not isinstance(http_client, httpx.Client): # pyright: ignore[reportUnnecessaryIsInstance] + raise TypeError( + f"Invalid `http_client` argument; Expected an instance of `httpx.Client` but got {type(http_client)}" + ) + + super().__init__( + version=version, + # cast to a valid type because mypy doesn't understand our type narrowing + timeout=cast(Timeout, timeout), + base_url=base_url, + max_retries=max_retries, + custom_query=custom_query, + custom_headers=custom_headers, + _strict_response_validation=_strict_response_validation, + ) + self._client = http_client or SyncHttpxClientWrapper( + base_url=base_url, + # cast to a valid type because mypy doesn't understand our type narrowing + timeout=cast(Timeout, timeout), + ) + + def is_closed(self) -> bool: + return self._client.is_closed + + def close(self) -> None: + """Close the underlying HTTPX client. + + The client will *not* be usable after this. + """ + # If an error is thrown while constructing a client, self._client + # may not be present + if hasattr(self, "_client"): + self._client.close() + + def __enter__(self: _T) -> _T: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.close() + + def _prepare_options( + self, + options: FinalRequestOptions, # noqa: ARG002 + ) -> FinalRequestOptions: + """Hook for mutating the given options""" + return options + + def _prepare_request( + self, + request: httpx.Request, # noqa: ARG002 + ) -> None: + """This method is used as a callback for mutating the `Request` object + after it has been constructed. + This is useful for cases where you want to add certain headers based off of + the request properties, e.g. `url`, `method` etc. + """ + return None + + @overload + def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: Literal[True], + stream_cls: Type[_StreamT], + ) -> _StreamT: ... + + @overload + def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: Literal[False] = False, + ) -> ResponseT: ... + + @overload + def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: bool = False, + stream_cls: Type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: ... + + def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: bool = False, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: + cast_to = self._maybe_override_cast_to(cast_to, options) + + # create a copy of the options we were given so that if the + # options are mutated later & we then retry, the retries are + # given the original options + input_options = model_copy(options) + if input_options.idempotency_key is None and input_options.method.lower() != "get": + # ensure the idempotency key is reused between requests + input_options.idempotency_key = self._idempotency_key() + + response: httpx.Response | None = None + max_retries = input_options.get_max_retries(self.max_retries) + + retries_taken = 0 + for retries_taken in range(max_retries + 1): + options = model_copy(input_options) + options = self._prepare_options(options) + + remaining_retries = max_retries - retries_taken + request = self._build_request(options, retries_taken=retries_taken) + self._prepare_request(request) + + kwargs: HttpxSendArgs = {} + if self.custom_auth is not None: + kwargs["auth"] = self.custom_auth + + if options.follow_redirects is not None: + kwargs["follow_redirects"] = options.follow_redirects + + log.debug("Sending HTTP Request: %s %s", request.method, request.url) + + response = None + try: + response = self._client.send( + request, + stream=stream or self._should_stream_response_body(request=request), + **kwargs, + ) + except httpx.TimeoutException as err: + log.debug("Encountered httpx.TimeoutException", exc_info=True) + + if remaining_retries > 0: + self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=None, + ) + continue + + log.debug("Raising timeout error") + raise APITimeoutError(request=request) from err + except Exception as err: + log.debug("Encountered Exception", exc_info=True) + + if remaining_retries > 0: + self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=None, + ) + continue + + log.debug("Raising connection error") + raise APIConnectionError(request=request) from err + + log.debug( + 'HTTP Response: %s %s "%i %s" %s', + request.method, + request.url, + response.status_code, + response.reason_phrase, + response.headers, + ) + + try: + response.raise_for_status() + except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code + log.debug("Encountered httpx.HTTPStatusError", exc_info=True) + + if remaining_retries > 0 and self._should_retry(err.response): + err.response.close() + self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=response, + ) + continue + + # If the response is streamed then we need to explicitly read the response + # to completion before attempting to access the response text. + if not err.response.is_closed: + err.response.read() + + log.debug("Re-raising status error") + raise self._make_status_error_from_response(err.response) from None + + break + + assert response is not None, "could not resolve response (should never happen)" + return self._process_response( + cast_to=cast_to, + options=options, + response=response, + stream=stream, + stream_cls=stream_cls, + retries_taken=retries_taken, + ) + + def _sleep_for_retry( + self, *, retries_taken: int, max_retries: int, options: FinalRequestOptions, response: httpx.Response | None + ) -> None: + remaining_retries = max_retries - retries_taken + if remaining_retries == 1: + log.debug("1 retry left") + else: + log.debug("%i retries left", remaining_retries) + + timeout = self._calculate_retry_timeout(remaining_retries, options, response.headers if response else None) + log.info("Retrying request to %s in %f seconds", options.url, timeout) + + time.sleep(timeout) + + def _process_response( + self, + *, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + response: httpx.Response, + stream: bool, + stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, + retries_taken: int = 0, + ) -> ResponseT: + origin = get_origin(cast_to) or cast_to + + if ( + inspect.isclass(origin) + and issubclass(origin, BaseAPIResponse) + # we only want to actually return the custom BaseAPIResponse class if we're + # returning the raw response, or if we're not streaming SSE, as if we're streaming + # SSE then `cast_to` doesn't actively reflect the type we need to parse into + and (not stream or bool(response.request.headers.get(RAW_RESPONSE_HEADER))) + ): + if not issubclass(origin, APIResponse): + raise TypeError(f"API Response types must subclass {APIResponse}; Received {origin}") + + response_cls = cast("type[BaseAPIResponse[Any]]", cast_to) + return cast( + ResponseT, + response_cls( + raw=response, + client=self, + cast_to=extract_response_type(response_cls), + stream=stream, + stream_cls=stream_cls, + options=options, + retries_taken=retries_taken, + ), + ) + + if cast_to == httpx.Response: + return cast(ResponseT, response) + + api_response = APIResponse( + raw=response, + client=self, + cast_to=cast("type[ResponseT]", cast_to), # pyright: ignore[reportUnnecessaryCast] + stream=stream, + stream_cls=stream_cls, + options=options, + retries_taken=retries_taken, + ) + if bool(response.request.headers.get(RAW_RESPONSE_HEADER)): + return cast(ResponseT, api_response) + + return api_response.parse() + + def _request_api_list( + self, + model: Type[object], + page: Type[SyncPageT], + options: FinalRequestOptions, + ) -> SyncPageT: + def _parser(resp: SyncPageT) -> SyncPageT: + resp._set_private_attributes( + client=self, + model=model, + options=options, + ) + return resp + + options.post_parser = _parser + + return self.request(page, options, stream=False) + + @overload + def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: Literal[False] = False, + ) -> ResponseT: ... + + @overload + def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: Literal[True], + stream_cls: type[_StreamT], + ) -> _StreamT: ... + + @overload + def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: bool, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: ... + + def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: bool = False, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: + opts = FinalRequestOptions.construct(method="get", url=path, **options) + # cast is required because mypy complains about returning Any even though + # it understands the type variables + return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) + + @overload + def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + content: BinaryTypes | 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, + content: BinaryTypes | 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, + content: BinaryTypes | 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, + content: BinaryTypes | None = None, + options: RequestOptions = {}, + files: RequestFiles | None = None, + stream: bool = False, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct( + method="post", url=path, json_data=body, content=content, 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, + content: BinaryTypes | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct( + method="patch", url=path, json_data=body, content=content, files=to_httpx_files(files), **options + ) + return self.request(cast_to, opts) + + def put( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + content: BinaryTypes | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct( + method="put", url=path, json_data=body, content=content, 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, + content: BinaryTypes | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) + return self.request(cast_to, opts) + + def get_api_list( + self, + path: str, + *, + model: Type[object], + page: Type[SyncPageT], + body: Body | None = None, + options: RequestOptions = {}, + method: str = "get", + ) -> SyncPageT: + opts = FinalRequestOptions.construct(method=method, url=path, json_data=body, **options) + return self._request_api_list(model, page, opts) + + +class _DefaultAsyncHttpxClient(httpx.AsyncClient): + def __init__(self, **kwargs: Any) -> None: + kwargs.setdefault("timeout", DEFAULT_TIMEOUT) + kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) + kwargs.setdefault("follow_redirects", True) + super().__init__(**kwargs) + + +try: + import httpx_aiohttp +except ImportError: + + class _DefaultAioHttpClient(httpx.AsyncClient): + def __init__(self, **_kwargs: Any) -> None: + raise RuntimeError("To use the aiohttp client you must have installed the package with the `aiohttp` extra") +else: + + class _DefaultAioHttpClient(httpx_aiohttp.HttpxAiohttpClient): # type: ignore + def __init__(self, **kwargs: Any) -> None: + kwargs.setdefault("timeout", DEFAULT_TIMEOUT) + kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) + kwargs.setdefault("follow_redirects", True) + + super().__init__(**kwargs) + + +if TYPE_CHECKING: + DefaultAsyncHttpxClient = httpx.AsyncClient + """An alias to `httpx.AsyncClient` that provides the same defaults that this SDK + uses internally. + + This is useful because overriding the `http_client` with your own instance of + `httpx.AsyncClient` will result in httpx's defaults being used, not ours. + """ + + DefaultAioHttpClient = httpx.AsyncClient + """An alias to `httpx.AsyncClient` that changes the default HTTP transport to `aiohttp`.""" +else: + DefaultAsyncHttpxClient = _DefaultAsyncHttpxClient + DefaultAioHttpClient = _DefaultAioHttpClient + + +class AsyncHttpxClientWrapper(DefaultAsyncHttpxClient): + def __del__(self) -> None: + if self.is_closed: + return + + try: + # TODO(someday): support non asyncio runtimes here + asyncio.get_running_loop().create_task(self.aclose()) + except Exception: + pass + + +class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]): + _client: httpx.AsyncClient + _default_stream_cls: type[AsyncStream[Any]] | None = None + + def __init__( + self, + *, + version: str, + base_url: str | URL, + _strict_response_validation: bool, + max_retries: int = DEFAULT_MAX_RETRIES, + timeout: float | Timeout | None | NotGiven = not_given, + http_client: httpx.AsyncClient | None = None, + custom_headers: Mapping[str, str] | None = None, + custom_query: Mapping[str, object] | None = None, + ) -> None: + if not is_given(timeout): + # if the user passed in a custom http client with a non-default + # timeout set then we use that timeout. + # + # note: there is an edge case here where the user passes in a client + # where they've explicitly set the timeout to match the default timeout + # as this check is structural, meaning that we'll think they didn't + # pass in a timeout and will ignore it + if http_client and http_client.timeout != HTTPX_DEFAULT_TIMEOUT: + timeout = http_client.timeout + else: + timeout = DEFAULT_TIMEOUT + + if http_client is not None and not isinstance(http_client, httpx.AsyncClient): # pyright: ignore[reportUnnecessaryIsInstance] + raise TypeError( + f"Invalid `http_client` argument; Expected an instance of `httpx.AsyncClient` but got {type(http_client)}" + ) + + super().__init__( + version=version, + base_url=base_url, + # cast to a valid type because mypy doesn't understand our type narrowing + timeout=cast(Timeout, timeout), + max_retries=max_retries, + custom_query=custom_query, + custom_headers=custom_headers, + _strict_response_validation=_strict_response_validation, + ) + self._client = http_client or AsyncHttpxClientWrapper( + base_url=base_url, + # cast to a valid type because mypy doesn't understand our type narrowing + timeout=cast(Timeout, timeout), + ) + + def is_closed(self) -> bool: + return self._client.is_closed + + async def close(self) -> None: + """Close the underlying HTTPX client. + + The client will *not* be usable after this. + """ + await self._client.aclose() + + async def __aenter__(self: _T) -> _T: + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + await self.close() + + async def _prepare_options( + self, + options: FinalRequestOptions, # noqa: ARG002 + ) -> FinalRequestOptions: + """Hook for mutating the given options""" + return options + + async def _prepare_request( + self, + request: httpx.Request, # noqa: ARG002 + ) -> None: + """This method is used as a callback for mutating the `Request` object + after it has been constructed. + This is useful for cases where you want to add certain headers based off of + the request properties, e.g. `url`, `method` etc. + """ + return None + + @overload + async def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: Literal[False] = False, + ) -> ResponseT: ... + + @overload + async def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: Literal[True], + stream_cls: type[_AsyncStreamT], + ) -> _AsyncStreamT: ... + + @overload + async def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: bool, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: ... + + async def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: bool = False, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: + if self._platform is None: + # `get_platform` can make blocking IO calls so we + # execute it earlier while we are in an async context + self._platform = await asyncify(get_platform)() + + cast_to = self._maybe_override_cast_to(cast_to, options) + + # create a copy of the options we were given so that if the + # options are mutated later & we then retry, the retries are + # given the original options + input_options = model_copy(options) + if input_options.idempotency_key is None and input_options.method.lower() != "get": + # ensure the idempotency key is reused between requests + input_options.idempotency_key = self._idempotency_key() + + response: httpx.Response | None = None + max_retries = input_options.get_max_retries(self.max_retries) + + retries_taken = 0 + for retries_taken in range(max_retries + 1): + options = model_copy(input_options) + options = await self._prepare_options(options) + + remaining_retries = max_retries - retries_taken + request = self._build_request(options, retries_taken=retries_taken) + await self._prepare_request(request) + + kwargs: HttpxSendArgs = {} + if self.custom_auth is not None: + kwargs["auth"] = self.custom_auth + + if options.follow_redirects is not None: + kwargs["follow_redirects"] = options.follow_redirects + + log.debug("Sending HTTP Request: %s %s", request.method, request.url) + + response = None + try: + response = await self._client.send( + request, + stream=stream or self._should_stream_response_body(request=request), + **kwargs, + ) + except httpx.TimeoutException as err: + log.debug("Encountered httpx.TimeoutException", exc_info=True) + + if remaining_retries > 0: + await self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=None, + ) + continue + + log.debug("Raising timeout error") + raise APITimeoutError(request=request) from err + except Exception as err: + log.debug("Encountered Exception", exc_info=True) + + if remaining_retries > 0: + await self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=None, + ) + continue + + log.debug("Raising connection error") + raise APIConnectionError(request=request) from err + + log.debug( + 'HTTP Response: %s %s "%i %s" %s', + request.method, + request.url, + response.status_code, + response.reason_phrase, + response.headers, + ) + + try: + response.raise_for_status() + except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code + log.debug("Encountered httpx.HTTPStatusError", exc_info=True) + + if remaining_retries > 0 and self._should_retry(err.response): + await err.response.aclose() + await self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=response, + ) + continue + + # If the response is streamed then we need to explicitly read the response + # to completion before attempting to access the response text. + if not err.response.is_closed: + await err.response.aread() + + log.debug("Re-raising status error") + raise self._make_status_error_from_response(err.response) from None + + break + + assert response is not None, "could not resolve response (should never happen)" + return await self._process_response( + cast_to=cast_to, + options=options, + response=response, + stream=stream, + stream_cls=stream_cls, + retries_taken=retries_taken, + ) + + async def _sleep_for_retry( + self, *, retries_taken: int, max_retries: int, options: FinalRequestOptions, response: httpx.Response | None + ) -> None: + remaining_retries = max_retries - retries_taken + if remaining_retries == 1: + log.debug("1 retry left") + else: + log.debug("%i retries left", remaining_retries) + + timeout = self._calculate_retry_timeout(remaining_retries, options, response.headers if response else None) + log.info("Retrying request to %s in %f seconds", options.url, timeout) + + await anyio.sleep(timeout) + + async def _process_response( + self, + *, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + response: httpx.Response, + stream: bool, + stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, + retries_taken: int = 0, + ) -> ResponseT: + origin = get_origin(cast_to) or cast_to + + if ( + inspect.isclass(origin) + and issubclass(origin, BaseAPIResponse) + # we only want to actually return the custom BaseAPIResponse class if we're + # returning the raw response, or if we're not streaming SSE, as if we're streaming + # SSE then `cast_to` doesn't actively reflect the type we need to parse into + and (not stream or bool(response.request.headers.get(RAW_RESPONSE_HEADER))) + ): + if not issubclass(origin, AsyncAPIResponse): + raise TypeError(f"API Response types must subclass {AsyncAPIResponse}; Received {origin}") + + response_cls = cast("type[BaseAPIResponse[Any]]", cast_to) + return cast( + "ResponseT", + response_cls( + raw=response, + client=self, + cast_to=extract_response_type(response_cls), + stream=stream, + stream_cls=stream_cls, + options=options, + retries_taken=retries_taken, + ), + ) + + if cast_to == httpx.Response: + return cast(ResponseT, response) + + api_response = AsyncAPIResponse( + raw=response, + client=self, + cast_to=cast("type[ResponseT]", cast_to), # pyright: ignore[reportUnnecessaryCast] + stream=stream, + stream_cls=stream_cls, + options=options, + retries_taken=retries_taken, + ) + if bool(response.request.headers.get(RAW_RESPONSE_HEADER)): + return cast(ResponseT, api_response) + + return await api_response.parse() + + def _request_api_list( + self, + model: Type[_T], + page: Type[AsyncPageT], + options: FinalRequestOptions, + ) -> AsyncPaginator[_T, AsyncPageT]: + return AsyncPaginator(client=self, options=options, page_cls=page, model=model) + + @overload + async def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: Literal[False] = False, + ) -> ResponseT: ... + + @overload + async def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: Literal[True], + stream_cls: type[_AsyncStreamT], + ) -> _AsyncStreamT: ... + + @overload + async def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: bool, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: ... + + async def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: bool = False, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: + opts = FinalRequestOptions.construct(method="get", url=path, **options) + return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) + + @overload + async def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + content: AsyncBinaryTypes | 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, + content: AsyncBinaryTypes | 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, + content: AsyncBinaryTypes | 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, + content: AsyncBinaryTypes | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + stream: bool = False, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct( + method="post", url=path, json_data=body, content=content, 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, + content: AsyncBinaryTypes | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct( + method="patch", + url=path, + json_data=body, + content=content, + files=await async_to_httpx_files(files), + **options, + ) + return await self.request(cast_to, opts) + + async def put( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + content: AsyncBinaryTypes | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct( + method="put", url=path, json_data=body, content=content, 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, + content: AsyncBinaryTypes | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) + return await self.request(cast_to, opts) + + def get_api_list( + 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/runloop_api_client/_client.py b/src/runloop_api_client/_client.py new file mode 100644 index 000000000..0f626950b --- /dev/null +++ b/src/runloop_api_client/_client.py @@ -0,0 +1,904 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import TYPE_CHECKING, Any, Mapping +from typing_extensions import Self, override + +import httpx + +from . import _exceptions +from ._qs import Querystring +from ._types import ( + Omit, + Timeout, + NotGiven, + Transport, + ProxiesTypes, + RequestOptions, + not_given, +) +from ._utils import is_given, get_async_library +from ._compat import cached_property +from ._version import __version__ +from ._streaming import Stream as Stream, AsyncStream as AsyncStream +from ._exceptions import RunloopError, APIStatusError +from ._base_client import ( + DEFAULT_MAX_RETRIES, + SyncAPIClient, + AsyncAPIClient, +) + +if TYPE_CHECKING: + from .resources import ( + agents, + objects, + secrets, + devboxes, + scenarios, + benchmarks, + blueprints, + mcp_configs, + repositories, + benchmark_jobs, + benchmark_runs, + gateway_configs, + network_policies, + ) + from .resources.agents import AgentsResource, AsyncAgentsResource + from .resources.objects import ObjectsResource, AsyncObjectsResource + from .resources.secrets import SecretsResource, AsyncSecretsResource + from .resources.benchmarks import BenchmarksResource, AsyncBenchmarksResource + from .resources.blueprints import BlueprintsResource, AsyncBlueprintsResource + from .resources.mcp_configs import McpConfigsResource, AsyncMcpConfigsResource + from .resources.repositories import RepositoriesResource, AsyncRepositoriesResource + from .resources.benchmark_jobs import BenchmarkJobsResource, AsyncBenchmarkJobsResource + from .resources.benchmark_runs import BenchmarkRunsResource, AsyncBenchmarkRunsResource + from .resources.gateway_configs import GatewayConfigsResource, AsyncGatewayConfigsResource + from .resources.network_policies import NetworkPoliciesResource, AsyncNetworkPoliciesResource + from .resources.devboxes.devboxes import DevboxesResource, AsyncDevboxesResource + from .resources.scenarios.scenarios import ScenariosResource, AsyncScenariosResource + +__all__ = ["Timeout", "Transport", "ProxiesTypes", "RequestOptions", "Runloop", "AsyncRunloop", "Client", "AsyncClient"] + + +class Runloop(SyncAPIClient): + # client options + bearer_token: str + + def __init__( + self, + *, + bearer_token: str | None = None, + base_url: str | httpx.URL | None = None, + timeout: float | Timeout | None | NotGiven = not_given, + max_retries: int = DEFAULT_MAX_RETRIES, + default_headers: Mapping[str, str] | None = None, + 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 Runloop client instance. + + This automatically infers the `bearer_token` argument from the `RUNLOOP_API_KEY` environment variable if it is not provided. + """ + if bearer_token is None: + bearer_token = os.environ.get("RUNLOOP_API_KEY") + if bearer_token is None: + raise RunloopError( + "The bearer_token client option must be set either by passing bearer_token to the client or by setting the RUNLOOP_API_KEY environment variable" + ) + self.bearer_token = bearer_token + + if base_url is None: + base_url = os.environ.get("RUNLOOP_BASE_URL") + if base_url is None: + base_url = f"https://api.runloop.ai" + + 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._idempotency_header = "x-request-id" + + @cached_property + def benchmarks(self) -> BenchmarksResource: + from .resources.benchmarks import BenchmarksResource + + return BenchmarksResource(self) + + @cached_property + def benchmark_runs(self) -> BenchmarkRunsResource: + from .resources.benchmark_runs import BenchmarkRunsResource + + return BenchmarkRunsResource(self) + + @cached_property + def benchmark_jobs(self) -> BenchmarkJobsResource: + from .resources.benchmark_jobs import BenchmarkJobsResource + + return BenchmarkJobsResource(self) + + @cached_property + def agents(self) -> AgentsResource: + from .resources.agents import AgentsResource + + return AgentsResource(self) + + @cached_property + def blueprints(self) -> BlueprintsResource: + from .resources.blueprints import BlueprintsResource + + return BlueprintsResource(self) + + @cached_property + def devboxes(self) -> DevboxesResource: + from .resources.devboxes import DevboxesResource + + return DevboxesResource(self) + + @cached_property + def scenarios(self) -> ScenariosResource: + from .resources.scenarios import ScenariosResource + + return ScenariosResource(self) + + @cached_property + def objects(self) -> ObjectsResource: + from .resources.objects import ObjectsResource + + return ObjectsResource(self) + + @cached_property + def repositories(self) -> RepositoriesResource: + from .resources.repositories import RepositoriesResource + + return RepositoriesResource(self) + + @cached_property + def secrets(self) -> SecretsResource: + from .resources.secrets import SecretsResource + + return SecretsResource(self) + + @cached_property + def network_policies(self) -> NetworkPoliciesResource: + from .resources.network_policies import NetworkPoliciesResource + + return NetworkPoliciesResource(self) + + @cached_property + def gateway_configs(self) -> GatewayConfigsResource: + from .resources.gateway_configs import GatewayConfigsResource + + return GatewayConfigsResource(self) + + @cached_property + def mcp_configs(self) -> McpConfigsResource: + from .resources.mcp_configs import McpConfigsResource + + return McpConfigsResource(self) + + @cached_property + def with_raw_response(self) -> RunloopWithRawResponse: + return RunloopWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> RunloopWithStreamedResponse: + return RunloopWithStreamedResponse(self) + + @property + @override + def qs(self) -> Querystring: + return Querystring(array_format="comma") + + @property + @override + def auth_headers(self) -> dict[str, str]: + bearer_token = self.bearer_token + return {"Authorization": f"Bearer {bearer_token}"} + + @property + @override + def default_headers(self) -> dict[str, str | Omit]: + return { + **super().default_headers, + "X-Stainless-Async": "false", + **self._custom_headers, + } + + def copy( + self, + *, + bearer_token: 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__( + bearer_token=bearer_token or self.bearer_token, + 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 AsyncRunloop(AsyncAPIClient): + # client options + bearer_token: str + + def __init__( + self, + *, + bearer_token: str | None = None, + base_url: str | httpx.URL | None = None, + timeout: float | Timeout | None | NotGiven = not_given, + max_retries: int = DEFAULT_MAX_RETRIES, + default_headers: Mapping[str, str] | None = None, + 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 AsyncRunloop client instance. + + This automatically infers the `bearer_token` argument from the `RUNLOOP_API_KEY` environment variable if it is not provided. + """ + if bearer_token is None: + bearer_token = os.environ.get("RUNLOOP_API_KEY") + if bearer_token is None: + raise RunloopError( + "The bearer_token client option must be set either by passing bearer_token to the client or by setting the RUNLOOP_API_KEY environment variable" + ) + self.bearer_token = bearer_token + + if base_url is None: + base_url = os.environ.get("RUNLOOP_BASE_URL") + if base_url is None: + base_url = f"https://api.runloop.ai" + + 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._idempotency_header = "x-request-id" + + @cached_property + def benchmarks(self) -> AsyncBenchmarksResource: + from .resources.benchmarks import AsyncBenchmarksResource + + return AsyncBenchmarksResource(self) + + @cached_property + def benchmark_runs(self) -> AsyncBenchmarkRunsResource: + from .resources.benchmark_runs import AsyncBenchmarkRunsResource + + return AsyncBenchmarkRunsResource(self) + + @cached_property + def benchmark_jobs(self) -> AsyncBenchmarkJobsResource: + from .resources.benchmark_jobs import AsyncBenchmarkJobsResource + + return AsyncBenchmarkJobsResource(self) + + @cached_property + def agents(self) -> AsyncAgentsResource: + from .resources.agents import AsyncAgentsResource + + return AsyncAgentsResource(self) + + @cached_property + def blueprints(self) -> AsyncBlueprintsResource: + from .resources.blueprints import AsyncBlueprintsResource + + return AsyncBlueprintsResource(self) + + @cached_property + def devboxes(self) -> AsyncDevboxesResource: + from .resources.devboxes import AsyncDevboxesResource + + return AsyncDevboxesResource(self) + + @cached_property + def scenarios(self) -> AsyncScenariosResource: + from .resources.scenarios import AsyncScenariosResource + + return AsyncScenariosResource(self) + + @cached_property + def objects(self) -> AsyncObjectsResource: + from .resources.objects import AsyncObjectsResource + + return AsyncObjectsResource(self) + + @cached_property + def repositories(self) -> AsyncRepositoriesResource: + from .resources.repositories import AsyncRepositoriesResource + + return AsyncRepositoriesResource(self) + + @cached_property + def secrets(self) -> AsyncSecretsResource: + from .resources.secrets import AsyncSecretsResource + + return AsyncSecretsResource(self) + + @cached_property + def network_policies(self) -> AsyncNetworkPoliciesResource: + from .resources.network_policies import AsyncNetworkPoliciesResource + + return AsyncNetworkPoliciesResource(self) + + @cached_property + def gateway_configs(self) -> AsyncGatewayConfigsResource: + from .resources.gateway_configs import AsyncGatewayConfigsResource + + return AsyncGatewayConfigsResource(self) + + @cached_property + def mcp_configs(self) -> AsyncMcpConfigsResource: + from .resources.mcp_configs import AsyncMcpConfigsResource + + return AsyncMcpConfigsResource(self) + + @cached_property + def with_raw_response(self) -> AsyncRunloopWithRawResponse: + return AsyncRunloopWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncRunloopWithStreamedResponse: + return AsyncRunloopWithStreamedResponse(self) + + @property + @override + def qs(self) -> Querystring: + return Querystring(array_format="comma") + + @property + @override + def auth_headers(self) -> dict[str, str]: + bearer_token = self.bearer_token + return {"Authorization": f"Bearer {bearer_token}"} + + @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, + *, + bearer_token: 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__( + bearer_token=bearer_token or self.bearer_token, + 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 RunloopWithRawResponse: + _client: Runloop + + def __init__(self, client: Runloop) -> None: + self._client = client + + @cached_property + def benchmarks(self) -> benchmarks.BenchmarksResourceWithRawResponse: + from .resources.benchmarks import BenchmarksResourceWithRawResponse + + return BenchmarksResourceWithRawResponse(self._client.benchmarks) + + @cached_property + def benchmark_runs(self) -> benchmark_runs.BenchmarkRunsResourceWithRawResponse: + from .resources.benchmark_runs import BenchmarkRunsResourceWithRawResponse + + return BenchmarkRunsResourceWithRawResponse(self._client.benchmark_runs) + + @cached_property + def benchmark_jobs(self) -> benchmark_jobs.BenchmarkJobsResourceWithRawResponse: + from .resources.benchmark_jobs import BenchmarkJobsResourceWithRawResponse + + return BenchmarkJobsResourceWithRawResponse(self._client.benchmark_jobs) + + @cached_property + def agents(self) -> agents.AgentsResourceWithRawResponse: + from .resources.agents import AgentsResourceWithRawResponse + + return AgentsResourceWithRawResponse(self._client.agents) + + @cached_property + def blueprints(self) -> blueprints.BlueprintsResourceWithRawResponse: + from .resources.blueprints import BlueprintsResourceWithRawResponse + + return BlueprintsResourceWithRawResponse(self._client.blueprints) + + @cached_property + def devboxes(self) -> devboxes.DevboxesResourceWithRawResponse: + from .resources.devboxes import DevboxesResourceWithRawResponse + + return DevboxesResourceWithRawResponse(self._client.devboxes) + + @cached_property + def scenarios(self) -> scenarios.ScenariosResourceWithRawResponse: + from .resources.scenarios import ScenariosResourceWithRawResponse + + return ScenariosResourceWithRawResponse(self._client.scenarios) + + @cached_property + def objects(self) -> objects.ObjectsResourceWithRawResponse: + from .resources.objects import ObjectsResourceWithRawResponse + + return ObjectsResourceWithRawResponse(self._client.objects) + + @cached_property + def repositories(self) -> repositories.RepositoriesResourceWithRawResponse: + from .resources.repositories import RepositoriesResourceWithRawResponse + + return RepositoriesResourceWithRawResponse(self._client.repositories) + + @cached_property + def secrets(self) -> secrets.SecretsResourceWithRawResponse: + from .resources.secrets import SecretsResourceWithRawResponse + + return SecretsResourceWithRawResponse(self._client.secrets) + + @cached_property + def network_policies(self) -> network_policies.NetworkPoliciesResourceWithRawResponse: + from .resources.network_policies import NetworkPoliciesResourceWithRawResponse + + return NetworkPoliciesResourceWithRawResponse(self._client.network_policies) + + @cached_property + def gateway_configs(self) -> gateway_configs.GatewayConfigsResourceWithRawResponse: + from .resources.gateway_configs import GatewayConfigsResourceWithRawResponse + + return GatewayConfigsResourceWithRawResponse(self._client.gateway_configs) + + @cached_property + def mcp_configs(self) -> mcp_configs.McpConfigsResourceWithRawResponse: + from .resources.mcp_configs import McpConfigsResourceWithRawResponse + + return McpConfigsResourceWithRawResponse(self._client.mcp_configs) + + +class AsyncRunloopWithRawResponse: + _client: AsyncRunloop + + def __init__(self, client: AsyncRunloop) -> None: + self._client = client + + @cached_property + def benchmarks(self) -> benchmarks.AsyncBenchmarksResourceWithRawResponse: + from .resources.benchmarks import AsyncBenchmarksResourceWithRawResponse + + return AsyncBenchmarksResourceWithRawResponse(self._client.benchmarks) + + @cached_property + def benchmark_runs(self) -> benchmark_runs.AsyncBenchmarkRunsResourceWithRawResponse: + from .resources.benchmark_runs import AsyncBenchmarkRunsResourceWithRawResponse + + return AsyncBenchmarkRunsResourceWithRawResponse(self._client.benchmark_runs) + + @cached_property + def benchmark_jobs(self) -> benchmark_jobs.AsyncBenchmarkJobsResourceWithRawResponse: + from .resources.benchmark_jobs import AsyncBenchmarkJobsResourceWithRawResponse + + return AsyncBenchmarkJobsResourceWithRawResponse(self._client.benchmark_jobs) + + @cached_property + def agents(self) -> agents.AsyncAgentsResourceWithRawResponse: + from .resources.agents import AsyncAgentsResourceWithRawResponse + + return AsyncAgentsResourceWithRawResponse(self._client.agents) + + @cached_property + def blueprints(self) -> blueprints.AsyncBlueprintsResourceWithRawResponse: + from .resources.blueprints import AsyncBlueprintsResourceWithRawResponse + + return AsyncBlueprintsResourceWithRawResponse(self._client.blueprints) + + @cached_property + def devboxes(self) -> devboxes.AsyncDevboxesResourceWithRawResponse: + from .resources.devboxes import AsyncDevboxesResourceWithRawResponse + + return AsyncDevboxesResourceWithRawResponse(self._client.devboxes) + + @cached_property + def scenarios(self) -> scenarios.AsyncScenariosResourceWithRawResponse: + from .resources.scenarios import AsyncScenariosResourceWithRawResponse + + return AsyncScenariosResourceWithRawResponse(self._client.scenarios) + + @cached_property + def objects(self) -> objects.AsyncObjectsResourceWithRawResponse: + from .resources.objects import AsyncObjectsResourceWithRawResponse + + return AsyncObjectsResourceWithRawResponse(self._client.objects) + + @cached_property + def repositories(self) -> repositories.AsyncRepositoriesResourceWithRawResponse: + from .resources.repositories import AsyncRepositoriesResourceWithRawResponse + + return AsyncRepositoriesResourceWithRawResponse(self._client.repositories) + + @cached_property + def secrets(self) -> secrets.AsyncSecretsResourceWithRawResponse: + from .resources.secrets import AsyncSecretsResourceWithRawResponse + + return AsyncSecretsResourceWithRawResponse(self._client.secrets) + + @cached_property + def network_policies(self) -> network_policies.AsyncNetworkPoliciesResourceWithRawResponse: + from .resources.network_policies import AsyncNetworkPoliciesResourceWithRawResponse + + return AsyncNetworkPoliciesResourceWithRawResponse(self._client.network_policies) + + @cached_property + def gateway_configs(self) -> gateway_configs.AsyncGatewayConfigsResourceWithRawResponse: + from .resources.gateway_configs import AsyncGatewayConfigsResourceWithRawResponse + + return AsyncGatewayConfigsResourceWithRawResponse(self._client.gateway_configs) + + @cached_property + def mcp_configs(self) -> mcp_configs.AsyncMcpConfigsResourceWithRawResponse: + from .resources.mcp_configs import AsyncMcpConfigsResourceWithRawResponse + + return AsyncMcpConfigsResourceWithRawResponse(self._client.mcp_configs) + + +class RunloopWithStreamedResponse: + _client: Runloop + + def __init__(self, client: Runloop) -> None: + self._client = client + + @cached_property + def benchmarks(self) -> benchmarks.BenchmarksResourceWithStreamingResponse: + from .resources.benchmarks import BenchmarksResourceWithStreamingResponse + + return BenchmarksResourceWithStreamingResponse(self._client.benchmarks) + + @cached_property + def benchmark_runs(self) -> benchmark_runs.BenchmarkRunsResourceWithStreamingResponse: + from .resources.benchmark_runs import BenchmarkRunsResourceWithStreamingResponse + + return BenchmarkRunsResourceWithStreamingResponse(self._client.benchmark_runs) + + @cached_property + def benchmark_jobs(self) -> benchmark_jobs.BenchmarkJobsResourceWithStreamingResponse: + from .resources.benchmark_jobs import BenchmarkJobsResourceWithStreamingResponse + + return BenchmarkJobsResourceWithStreamingResponse(self._client.benchmark_jobs) + + @cached_property + def agents(self) -> agents.AgentsResourceWithStreamingResponse: + from .resources.agents import AgentsResourceWithStreamingResponse + + return AgentsResourceWithStreamingResponse(self._client.agents) + + @cached_property + def blueprints(self) -> blueprints.BlueprintsResourceWithStreamingResponse: + from .resources.blueprints import BlueprintsResourceWithStreamingResponse + + return BlueprintsResourceWithStreamingResponse(self._client.blueprints) + + @cached_property + def devboxes(self) -> devboxes.DevboxesResourceWithStreamingResponse: + from .resources.devboxes import DevboxesResourceWithStreamingResponse + + return DevboxesResourceWithStreamingResponse(self._client.devboxes) + + @cached_property + def scenarios(self) -> scenarios.ScenariosResourceWithStreamingResponse: + from .resources.scenarios import ScenariosResourceWithStreamingResponse + + return ScenariosResourceWithStreamingResponse(self._client.scenarios) + + @cached_property + def objects(self) -> objects.ObjectsResourceWithStreamingResponse: + from .resources.objects import ObjectsResourceWithStreamingResponse + + return ObjectsResourceWithStreamingResponse(self._client.objects) + + @cached_property + def repositories(self) -> repositories.RepositoriesResourceWithStreamingResponse: + from .resources.repositories import RepositoriesResourceWithStreamingResponse + + return RepositoriesResourceWithStreamingResponse(self._client.repositories) + + @cached_property + def secrets(self) -> secrets.SecretsResourceWithStreamingResponse: + from .resources.secrets import SecretsResourceWithStreamingResponse + + return SecretsResourceWithStreamingResponse(self._client.secrets) + + @cached_property + def network_policies(self) -> network_policies.NetworkPoliciesResourceWithStreamingResponse: + from .resources.network_policies import NetworkPoliciesResourceWithStreamingResponse + + return NetworkPoliciesResourceWithStreamingResponse(self._client.network_policies) + + @cached_property + def gateway_configs(self) -> gateway_configs.GatewayConfigsResourceWithStreamingResponse: + from .resources.gateway_configs import GatewayConfigsResourceWithStreamingResponse + + return GatewayConfigsResourceWithStreamingResponse(self._client.gateway_configs) + + @cached_property + def mcp_configs(self) -> mcp_configs.McpConfigsResourceWithStreamingResponse: + from .resources.mcp_configs import McpConfigsResourceWithStreamingResponse + + return McpConfigsResourceWithStreamingResponse(self._client.mcp_configs) + + +class AsyncRunloopWithStreamedResponse: + _client: AsyncRunloop + + def __init__(self, client: AsyncRunloop) -> None: + self._client = client + + @cached_property + def benchmarks(self) -> benchmarks.AsyncBenchmarksResourceWithStreamingResponse: + from .resources.benchmarks import AsyncBenchmarksResourceWithStreamingResponse + + return AsyncBenchmarksResourceWithStreamingResponse(self._client.benchmarks) + + @cached_property + def benchmark_runs(self) -> benchmark_runs.AsyncBenchmarkRunsResourceWithStreamingResponse: + from .resources.benchmark_runs import AsyncBenchmarkRunsResourceWithStreamingResponse + + return AsyncBenchmarkRunsResourceWithStreamingResponse(self._client.benchmark_runs) + + @cached_property + def benchmark_jobs(self) -> benchmark_jobs.AsyncBenchmarkJobsResourceWithStreamingResponse: + from .resources.benchmark_jobs import AsyncBenchmarkJobsResourceWithStreamingResponse + + return AsyncBenchmarkJobsResourceWithStreamingResponse(self._client.benchmark_jobs) + + @cached_property + def agents(self) -> agents.AsyncAgentsResourceWithStreamingResponse: + from .resources.agents import AsyncAgentsResourceWithStreamingResponse + + return AsyncAgentsResourceWithStreamingResponse(self._client.agents) + + @cached_property + def blueprints(self) -> blueprints.AsyncBlueprintsResourceWithStreamingResponse: + from .resources.blueprints import AsyncBlueprintsResourceWithStreamingResponse + + return AsyncBlueprintsResourceWithStreamingResponse(self._client.blueprints) + + @cached_property + def devboxes(self) -> devboxes.AsyncDevboxesResourceWithStreamingResponse: + from .resources.devboxes import AsyncDevboxesResourceWithStreamingResponse + + return AsyncDevboxesResourceWithStreamingResponse(self._client.devboxes) + + @cached_property + def scenarios(self) -> scenarios.AsyncScenariosResourceWithStreamingResponse: + from .resources.scenarios import AsyncScenariosResourceWithStreamingResponse + + return AsyncScenariosResourceWithStreamingResponse(self._client.scenarios) + + @cached_property + def objects(self) -> objects.AsyncObjectsResourceWithStreamingResponse: + from .resources.objects import AsyncObjectsResourceWithStreamingResponse + + return AsyncObjectsResourceWithStreamingResponse(self._client.objects) + + @cached_property + def repositories(self) -> repositories.AsyncRepositoriesResourceWithStreamingResponse: + from .resources.repositories import AsyncRepositoriesResourceWithStreamingResponse + + return AsyncRepositoriesResourceWithStreamingResponse(self._client.repositories) + + @cached_property + def secrets(self) -> secrets.AsyncSecretsResourceWithStreamingResponse: + from .resources.secrets import AsyncSecretsResourceWithStreamingResponse + + return AsyncSecretsResourceWithStreamingResponse(self._client.secrets) + + @cached_property + def network_policies(self) -> network_policies.AsyncNetworkPoliciesResourceWithStreamingResponse: + from .resources.network_policies import AsyncNetworkPoliciesResourceWithStreamingResponse + + return AsyncNetworkPoliciesResourceWithStreamingResponse(self._client.network_policies) + + @cached_property + def gateway_configs(self) -> gateway_configs.AsyncGatewayConfigsResourceWithStreamingResponse: + from .resources.gateway_configs import AsyncGatewayConfigsResourceWithStreamingResponse + + return AsyncGatewayConfigsResourceWithStreamingResponse(self._client.gateway_configs) + + @cached_property + def mcp_configs(self) -> mcp_configs.AsyncMcpConfigsResourceWithStreamingResponse: + from .resources.mcp_configs import AsyncMcpConfigsResourceWithStreamingResponse + + return AsyncMcpConfigsResourceWithStreamingResponse(self._client.mcp_configs) + + +Client = Runloop + +AsyncClient = AsyncRunloop diff --git a/src/runloop_api_client/_compat.py b/src/runloop_api_client/_compat.py new file mode 100644 index 000000000..786ff42ad --- /dev/null +++ b/src/runloop_api_client/_compat.py @@ -0,0 +1,219 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload +from datetime import date, datetime +from typing_extensions import Self, Literal + +import pydantic +from pydantic.fields import FieldInfo + +from ._types import IncEx, StrBytesIntFloat + +_T = TypeVar("_T") +_ModelT = TypeVar("_ModelT", bound=pydantic.BaseModel) + +# --------------- Pydantic v2, v3 compatibility --------------- + +# Pyright incorrectly reports some of our functions as overriding a method when they don't +# pyright: reportIncompatibleMethodOverride=false + +PYDANTIC_V1 = pydantic.VERSION.startswith("1.") + +if TYPE_CHECKING: + + def parse_date(value: date | StrBytesIntFloat) -> date: # noqa: ARG001 + ... + + def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime: # noqa: ARG001 + ... + + def get_args(t: type[Any]) -> tuple[Any, ...]: # noqa: ARG001 + ... + + def is_union(tp: type[Any] | None) -> bool: # noqa: ARG001 + ... + + def get_origin(t: type[Any]) -> type[Any] | None: # noqa: ARG001 + ... + + def is_literal_type(type_: type[Any]) -> bool: # noqa: ARG001 + ... + + def is_typeddict(type_: type[Any]) -> bool: # noqa: ARG001 + ... + +else: + # v1 re-exports + if PYDANTIC_V1: + from pydantic.typing import ( + get_args as get_args, + is_union as is_union, + get_origin as get_origin, + is_typeddict as is_typeddict, + is_literal_type as is_literal_type, + ) + from pydantic.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime + else: + from ._utils import ( + get_args as get_args, + is_union as is_union, + get_origin as get_origin, + parse_date as parse_date, + is_typeddict as is_typeddict, + parse_datetime as parse_datetime, + is_literal_type as is_literal_type, + ) + + +# refactored config +if TYPE_CHECKING: + from pydantic import ConfigDict as ConfigDict +else: + if PYDANTIC_V1: + # TODO: provide an error message here? + ConfigDict = None + else: + from pydantic import ConfigDict as ConfigDict + + +# renamed methods / properties +def parse_obj(model: type[_ModelT], value: object) -> _ModelT: + if PYDANTIC_V1: + return cast(_ModelT, model.parse_obj(value)) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] + else: + return model.model_validate(value) + + +def field_is_required(field: FieldInfo) -> bool: + if PYDANTIC_V1: + return field.required # type: ignore + return field.is_required() + + +def field_get_default(field: FieldInfo) -> Any: + value = field.get_default() + if PYDANTIC_V1: + return value + from pydantic_core import PydanticUndefined + + if value == PydanticUndefined: + return None + return value + + +def field_outer_type(field: FieldInfo) -> Any: + if PYDANTIC_V1: + return field.outer_type_ # type: ignore + return field.annotation + + +def get_model_config(model: type[pydantic.BaseModel]) -> Any: + if PYDANTIC_V1: + return model.__config__ # type: ignore + return model.model_config + + +def get_model_fields(model: type[pydantic.BaseModel]) -> dict[str, FieldInfo]: + if PYDANTIC_V1: + return model.__fields__ # type: ignore + return model.model_fields + + +def model_copy(model: _ModelT, *, deep: bool = False) -> _ModelT: + if PYDANTIC_V1: + return model.copy(deep=deep) # type: ignore + return model.model_copy(deep=deep) + + +def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str: + if PYDANTIC_V1: + return model.json(indent=indent) # type: ignore + return model.model_dump_json(indent=indent) + + +def model_dump( + model: pydantic.BaseModel, + *, + exclude: IncEx | None = None, + exclude_unset: bool = False, + exclude_defaults: bool = False, + warnings: bool = True, + mode: Literal["json", "python"] = "python", + by_alias: bool | None = None, +) -> dict[str, Any]: + if (not PYDANTIC_V1) or hasattr(model, "model_dump"): + return model.model_dump( + mode=mode, + exclude=exclude, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + # warnings are not supported in Pydantic v1 + warnings=True if PYDANTIC_V1 else warnings, + by_alias=by_alias, + ) + return cast( + "dict[str, Any]", + model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast] + exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, by_alias=bool(by_alias) + ), + ) + + +def model_parse(model: type[_ModelT], data: Any) -> _ModelT: + if PYDANTIC_V1: + return model.parse_obj(data) # pyright: ignore[reportDeprecated] + return model.model_validate(data) + + +# generic models +if TYPE_CHECKING: + + class GenericModel(pydantic.BaseModel): ... + +else: + if PYDANTIC_V1: + import pydantic.generics + + class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel): ... + else: + # there no longer needs to be a distinction in v2 but + # we still have to create our own subclass to avoid + # inconsistent MRO ordering errors + class GenericModel(pydantic.BaseModel): ... + + +# cached properties +if TYPE_CHECKING: + cached_property = property + + # we define a separate type (copied from typeshed) + # that represents that `cached_property` is `set`able + # at runtime, which differs from `@property`. + # + # this is a separate type as editors likely special case + # `@property` and we don't want to cause issues just to have + # more helpful internal types. + + class typed_cached_property(Generic[_T]): + func: Callable[[Any], _T] + attrname: str | None + + def __init__(self, func: Callable[[Any], _T]) -> None: ... + + @overload + def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: ... + + @overload + def __get__(self, instance: object, owner: type[Any] | None = None) -> _T: ... + + def __get__(self, instance: object, owner: type[Any] | None = None) -> _T | Self: + raise NotImplementedError() + + def __set_name__(self, owner: type[Any], name: str) -> None: ... + + # __set__ is not defined at runtime, but @cached_property is designed to be settable + def __set__(self, instance: object, value: _T) -> None: ... +else: + from functools import cached_property as cached_property + + typed_cached_property = cached_property diff --git a/src/runloop_api_client/_constants.py b/src/runloop_api_client/_constants.py new file mode 100644 index 000000000..4fe93f3ea --- /dev/null +++ b/src/runloop_api_client/_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 30 seconds +DEFAULT_TIMEOUT = httpx.Timeout(timeout=30, connect=5.0) +DEFAULT_MAX_RETRIES = 5 +DEFAULT_CONNECTION_LIMITS = httpx.Limits(max_connections=100, max_keepalive_connections=20) + +INITIAL_RETRY_DELAY = 1.0 +MAX_RETRY_DELAY = 60.0 diff --git a/src/runloop_api_client/_exceptions.py b/src/runloop_api_client/_exceptions.py new file mode 100644 index 000000000..dd4f7d276 --- /dev/null +++ b/src/runloop_api_client/_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 RunloopError(Exception): + pass + + +class APIError(RunloopError): + 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/runloop_api_client/_files.py b/src/runloop_api_client/_files.py new file mode 100644 index 000000000..10f7978c8 --- /dev/null +++ b/src/runloop_api_client/_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/runloopai/api-client-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/runloop_api_client/_models.py b/src/runloop_api_client/_models.py new file mode 100644 index 000000000..29070e055 --- /dev/null +++ b/src/runloop_api_client/_models.py @@ -0,0 +1,872 @@ +from __future__ import annotations + +import os +import inspect +import weakref +from typing import ( + IO, + TYPE_CHECKING, + Any, + Type, + Union, + Generic, + TypeVar, + Callable, + Iterable, + Optional, + AsyncIterable, + cast, +) +from datetime import date, datetime +from typing_extensions import ( + List, + Unpack, + Literal, + ClassVar, + Protocol, + Required, + ParamSpec, + TypedDict, + TypeGuard, + final, + override, + runtime_checkable, +) + +import pydantic +from pydantic.fields import FieldInfo + +from ._types import ( + Body, + IncEx, + Query, + ModelT, + Headers, + Timeout, + NotGiven, + AnyMapping, + HttpxRequestFiles, +) +from ._utils import ( + PropertyInfo, + is_list, + is_given, + json_safe, + lru_cache, + is_mapping, + parse_date, + coerce_boolean, + parse_datetime, + strip_not_given, + extract_type_arg, + is_annotated_type, + is_type_alias_type, + strip_annotated_type, +) +from ._compat import ( + PYDANTIC_V1, + ConfigDict, + GenericModel as BaseGenericModel, + get_args, + is_union, + parse_obj, + get_origin, + is_literal_type, + get_model_config, + get_model_fields, + field_get_default, +) +from ._constants import RAW_RESPONSE_HEADER + +if TYPE_CHECKING: + from pydantic_core.core_schema import ModelField, ModelSchema, LiteralSchema, ModelFieldsSchema + +__all__ = ["BaseModel", "GenericModel"] + +_T = TypeVar("_T") +_BaseModelT = TypeVar("_BaseModelT", bound="BaseModel") + +P = ParamSpec("P") + + +@runtime_checkable +class _ConfigProtocol(Protocol): + allow_population_by_field_name: bool + + +class BaseModel(pydantic.BaseModel): + if PYDANTIC_V1: + + @property + @override + def model_fields_set(self) -> set[str]: + # a forwards-compat shim for pydantic v2 + return self.__fields_set__ # type: ignore + + class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated] + extra: Any = pydantic.Extra.allow # type: ignore + else: + model_config: ClassVar[ConfigDict] = ConfigDict( + extra="allow", defer_build=coerce_boolean(os.environ.get("DEFER_PYDANTIC_BUILD", "true")) + ) + + def to_dict( + self, + *, + mode: Literal["json", "python"] = "python", + use_api_names: bool = True, + exclude_unset: bool = True, + exclude_defaults: bool = False, + exclude_none: bool = False, + warnings: bool = True, + ) -> dict[str, object]: + """Recursively generate a dictionary representation of the model, optionally specifying which fields to include or exclude. + + By default, fields that were not set by the API will not be included, + and keys will match the API response, *not* the property names from the model. + + For example, if the API responds with `"fooBar": true` but we've defined a `foo_bar: bool` property, + the output will use the `"fooBar"` key (unless `use_api_names=False` is passed). + + Args: + mode: + If mode is 'json', the dictionary will only contain JSON serializable types. e.g. `datetime` will be turned into a string, `"2024-3-22T18:11:19.117000Z"`. + If mode is 'python', the dictionary may contain any Python objects. e.g. `datetime(2024, 3, 22)` + + use_api_names: Whether to use the key that the API responded with or the property name. Defaults to `True`. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that are set to their default value from the output. + exclude_none: Whether to exclude fields that have a value of `None` from the output. + warnings: Whether to log warnings when invalid fields are encountered. This is only supported in Pydantic v2. + """ + return self.model_dump( + mode=mode, + by_alias=use_api_names, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + warnings=warnings, + ) + + def to_json( + self, + *, + indent: int | None = 2, + use_api_names: bool = True, + exclude_unset: bool = True, + exclude_defaults: bool = False, + exclude_none: bool = False, + warnings: bool = True, + ) -> str: + """Generates a JSON string representing this model as it would be received from or sent to the API (but with indentation). + + By default, fields that were not set by the API will not be included, + and keys will match the API response, *not* the property names from the model. + + For example, if the API responds with `"fooBar": true` but we've defined a `foo_bar: bool` property, + the output will use the `"fooBar"` key (unless `use_api_names=False` is passed). + + Args: + indent: Indentation to use in the JSON output. If `None` is passed, the output will be compact. Defaults to `2` + use_api_names: Whether to use the key that the API responded with or the property name. Defaults to `True`. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that have the default value. + exclude_none: Whether to exclude fields that have a value of `None`. + warnings: Whether to show any warnings that occurred during serialization. This is only supported in Pydantic v2. + """ + return self.model_dump_json( + indent=indent, + by_alias=use_api_names, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + warnings=warnings, + ) + + @override + def __str__(self) -> str: + # mypy complains about an invalid self arg + return f"{self.__repr_name__()}({self.__repr_str__(', ')})" # type: ignore[misc] + + # Override the 'construct' method in a way that supports recursive parsing without validation. + # Based on https://github.com/samuelcolvin/pydantic/issues/1168#issuecomment-817742836. + @classmethod + @override + def construct( # pyright: ignore[reportIncompatibleMethodOverride] + __cls: Type[ModelT], + _fields_set: set[str] | None = None, + **values: object, + ) -> ModelT: + m = __cls.__new__(__cls) + fields_values: dict[str, object] = {} + + config = get_model_config(__cls) + populate_by_name = ( + config.allow_population_by_field_name + if isinstance(config, _ConfigProtocol) + else config.get("populate_by_name") + ) + + if _fields_set is None: + _fields_set = set() + + model_fields = get_model_fields(__cls) + for name, field in model_fields.items(): + key = field.alias + if key is None or (key not in values and populate_by_name): + key = name + + if key in values: + fields_values[name] = _construct_field(value=values[key], field=field, key=key) + _fields_set.add(name) + else: + fields_values[name] = field_get_default(field) + + extra_field_type = _get_extra_fields_type(__cls) + + _extra = {} + for key, value in values.items(): + if key not in model_fields: + parsed = construct_type(value=value, type_=extra_field_type) if extra_field_type is not None else value + + if PYDANTIC_V1: + _fields_set.add(key) + fields_values[key] = parsed + else: + _extra[key] = parsed + + object.__setattr__(m, "__dict__", fields_values) + + if PYDANTIC_V1: + # init_private_attributes() does not exist in v2 + m._init_private_attributes() # type: ignore + + # copied from Pydantic v1's `construct()` method + object.__setattr__(m, "__fields_set__", _fields_set) + else: + # these properties are copied from Pydantic's `model_construct()` method + object.__setattr__(m, "__pydantic_private__", None) + object.__setattr__(m, "__pydantic_extra__", _extra) + object.__setattr__(m, "__pydantic_fields_set__", _fields_set) + + return m + + if not TYPE_CHECKING: + # type checkers incorrectly complain about this assignment + # because the type signatures are technically different + # although not in practice + model_construct = construct + + if PYDANTIC_V1: + # we define aliases for some of the new pydantic v2 methods so + # that we can just document these methods without having to specify + # a specific pydantic version as some users may not know which + # pydantic version they are currently using + + @override + def model_dump( + self, + *, + mode: Literal["json", "python"] | str = "python", + include: IncEx | None = None, + exclude: IncEx | None = None, + context: Any | None = None, + by_alias: bool | None = None, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + exclude_computed_fields: bool = False, + round_trip: bool = False, + warnings: bool | Literal["none", "warn", "error"] = True, + fallback: Callable[[Any], Any] | None = None, + serialize_as_any: bool = False, + ) -> dict[str, Any]: + """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump + + Generate a dictionary representation of the model, optionally specifying which fields to include or exclude. + + Args: + mode: The mode in which `to_python` should run. + If mode is 'json', the output will only contain JSON serializable types. + If mode is 'python', the output may contain non-JSON-serializable Python objects. + include: A set of fields to include in the output. + exclude: A set of fields to exclude from the output. + context: Additional context to pass to the serializer. + by_alias: Whether to use the field's alias in the dictionary key if defined. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that are set to their default value. + exclude_none: Whether to exclude fields that have a value of `None`. + exclude_computed_fields: Whether to exclude computed fields. + While this can be useful for round-tripping, it is usually recommended to use the dedicated + `round_trip` parameter instead. + round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T]. + warnings: How to handle serialization errors. False/"none" ignores them, True/"warn" logs errors, + "error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError]. + fallback: A function to call when an unknown value is encountered. If not provided, + a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised. + serialize_as_any: Whether to serialize fields with duck-typing serialization behavior. + + Returns: + A dictionary representation of the model. + """ + if mode not in {"json", "python"}: + raise ValueError("mode must be either 'json' or 'python'") + if round_trip != False: + raise ValueError("round_trip is only supported in Pydantic v2") + if warnings != True: + raise ValueError("warnings is only supported in Pydantic v2") + if context is not None: + raise ValueError("context is only supported in Pydantic v2") + if serialize_as_any != False: + raise ValueError("serialize_as_any is only supported in Pydantic v2") + if fallback is not None: + raise ValueError("fallback is only supported in Pydantic v2") + if exclude_computed_fields != False: + raise ValueError("exclude_computed_fields is only supported in Pydantic v2") + dumped = super().dict( # pyright: ignore[reportDeprecated] + include=include, + exclude=exclude, + by_alias=by_alias if by_alias is not None else False, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + + return cast("dict[str, Any]", json_safe(dumped)) if mode == "json" else dumped + + @override + def model_dump_json( + self, + *, + indent: int | None = None, + ensure_ascii: bool = False, + include: IncEx | None = None, + exclude: IncEx | None = None, + context: Any | None = None, + by_alias: bool | None = None, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + exclude_computed_fields: bool = False, + round_trip: bool = False, + warnings: bool | Literal["none", "warn", "error"] = True, + fallback: Callable[[Any], Any] | None = None, + serialize_as_any: bool = False, + ) -> str: + """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump_json + + Generates a JSON representation of the model using Pydantic's `to_json` method. + + Args: + indent: Indentation to use in the JSON output. If None is passed, the output will be compact. + include: Field(s) to include in the JSON output. Can take either a string or set of strings. + exclude: Field(s) to exclude from the JSON output. Can take either a string or set of strings. + by_alias: Whether to serialize using field aliases. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that have the default value. + exclude_none: Whether to exclude fields that have a value of `None`. + round_trip: Whether to use serialization/deserialization between JSON and class instance. + warnings: Whether to show any warnings that occurred during serialization. + + Returns: + A JSON string representation of the model. + """ + if round_trip != False: + raise ValueError("round_trip is only supported in Pydantic v2") + if warnings != True: + raise ValueError("warnings is only supported in Pydantic v2") + if context is not None: + raise ValueError("context is only supported in Pydantic v2") + if serialize_as_any != False: + raise ValueError("serialize_as_any is only supported in Pydantic v2") + if fallback is not None: + raise ValueError("fallback is only supported in Pydantic v2") + if ensure_ascii != False: + raise ValueError("ensure_ascii is only supported in Pydantic v2") + if exclude_computed_fields != False: + raise ValueError("exclude_computed_fields is only supported in Pydantic v2") + return super().json( # type: ignore[reportDeprecated] + indent=indent, + include=include, + exclude=exclude, + by_alias=by_alias if by_alias is not None else False, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + + +def _construct_field(value: object, field: FieldInfo, key: str) -> object: + if value is None: + return field_get_default(field) + + if PYDANTIC_V1: + type_ = cast(type, field.outer_type_) # type: ignore + else: + type_ = field.annotation # type: ignore + + if type_ is None: + raise RuntimeError(f"Unexpected field type is None for {key}") + + return construct_type(value=value, type_=type_, metadata=getattr(field, "metadata", None)) + + +def _get_extra_fields_type(cls: type[pydantic.BaseModel]) -> type | None: + if PYDANTIC_V1: + # TODO + return None + + schema = cls.__pydantic_core_schema__ + if schema["type"] == "model": + fields = schema["schema"] + if fields["type"] == "model-fields": + extras = fields.get("extras_schema") + if extras and "cls" in extras: + # mypy can't narrow the type + return extras["cls"] # type: ignore[no-any-return] + + return None + + +def is_basemodel(type_: type) -> bool: + """Returns whether or not the given type is either a `BaseModel` or a union of `BaseModel`""" + if is_union(type_): + for variant in get_args(type_): + if is_basemodel(variant): + return True + + return False + + return is_basemodel_type(type_) + + +def is_basemodel_type(type_: type) -> TypeGuard[type[BaseModel] | type[GenericModel]]: + origin = get_origin(type_) or type_ + if not inspect.isclass(origin): + return False + return issubclass(origin, BaseModel) or issubclass(origin, GenericModel) + + +def build( + base_model_cls: Callable[P, _BaseModelT], + *args: P.args, + **kwargs: P.kwargs, +) -> _BaseModelT: + """Construct a BaseModel class without validation. + + This is useful for cases where you need to instantiate a `BaseModel` + from an API response as this provides type-safe params which isn't supported + by helpers like `construct_type()`. + + ```py + build(MyModel, my_field_a="foo", my_field_b=123) + ``` + """ + if args: + raise TypeError( + "Received positional arguments which are not supported; Keyword arguments must be used instead", + ) + + return cast(_BaseModelT, construct_type(type_=base_model_cls, value=kwargs)) + + +def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T: + """Loose coercion to the expected type with construction of nested values. + + Note: the returned value from this function is not guaranteed to match the + given type. + """ + return cast(_T, construct_type(value=value, type_=type_)) + + +def construct_type(*, value: object, type_: object, metadata: Optional[List[Any]] = None) -> object: + """Loose coercion to the expected type with construction of nested values. + + If the given value does not match the expected type then it is returned as-is. + """ + + # store a reference to the original type we were given before we extract any inner + # types so that we can properly resolve forward references in `TypeAliasType` annotations + original_type = None + + # we allow `object` as the input type because otherwise, passing things like + # `Literal['value']` will be reported as a type error by type checkers + type_ = cast("type[object]", type_) + if is_type_alias_type(type_): + original_type = type_ # type: ignore[unreachable] + type_ = type_.__value__ # type: ignore[unreachable] + + # unwrap `Annotated[T, ...]` -> `T` + if metadata is not None and len(metadata) > 0: + meta: tuple[Any, ...] = tuple(metadata) + elif is_annotated_type(type_): + meta = get_args(type_)[1:] + type_ = extract_type_arg(type_, 0) + else: + meta = tuple() + + # we need to use the origin class for any types that are subscripted generics + # e.g. Dict[str, object] + origin = get_origin(type_) or type_ + args = get_args(type_) + + if is_union(origin): + try: + return validate_type(type_=cast("type[object]", original_type or type_), value=value) + except Exception: + pass + + # if the type is a discriminated union then we want to construct the right variant + # in the union, even if the data doesn't match exactly, otherwise we'd break code + # that relies on the constructed class types, e.g. + # + # class FooType: + # kind: Literal['foo'] + # value: str + # + # class BarType: + # kind: Literal['bar'] + # value: int + # + # without this block, if the data we get is something like `{'kind': 'bar', 'value': 'foo'}` then + # we'd end up constructing `FooType` when it should be `BarType`. + discriminator = _build_discriminated_union_meta(union=type_, meta_annotations=meta) + if discriminator and is_mapping(value): + variant_value = value.get(discriminator.field_alias_from or discriminator.field_name) + if variant_value and isinstance(variant_value, str): + variant_type = discriminator.mapping.get(variant_value) + if variant_type: + return construct_type(type_=variant_type, value=value) + + # if the data is not valid, use the first variant that doesn't fail while deserializing + for variant in args: + try: + return construct_type(value=value, type_=variant) + except Exception: + continue + + raise RuntimeError(f"Could not convert data into a valid instance of {type_}") + + if origin == dict: + if not is_mapping(value): + return value + + _, items_type = get_args(type_) # Dict[_, items_type] + return {key: construct_type(value=item, type_=items_type) for key, item in value.items()} + + if ( + not is_literal_type(type_) + and inspect.isclass(origin) + and (issubclass(origin, BaseModel) or issubclass(origin, GenericModel)) + ): + if is_list(value): + return [cast(Any, type_).construct(**entry) if is_mapping(entry) else entry for entry in value] + + if is_mapping(value): + if issubclass(type_, BaseModel): + return type_.construct(**value) # type: ignore[arg-type] + + return cast(Any, type_).construct(**value) + + if origin == list: + if not is_list(value): + return value + + inner_type = args[0] # List[inner_type] + return [construct_type(value=entry, type_=inner_type) for entry in value] + + if origin == float: + if isinstance(value, int): + coerced = float(value) + if coerced != value: + return value + return coerced + + return value + + if type_ == datetime: + try: + return parse_datetime(value) # type: ignore + except Exception: + return value + + if type_ == date: + try: + return parse_date(value) # type: ignore + except Exception: + return value + + return value + + +@runtime_checkable +class CachedDiscriminatorType(Protocol): + __discriminator__: DiscriminatorDetails + + +DISCRIMINATOR_CACHE: weakref.WeakKeyDictionary[type, DiscriminatorDetails] = weakref.WeakKeyDictionary() + + +class DiscriminatorDetails: + field_name: str + """The name of the discriminator field in the variant class, e.g. + + ```py + class Foo(BaseModel): + type: Literal['foo'] + ``` + + Will result in field_name='type' + """ + + field_alias_from: str | None + """The name of the discriminator field in the API response, e.g. + + ```py + class Foo(BaseModel): + type: Literal['foo'] = Field(alias='type_from_api') + ``` + + Will result in field_alias_from='type_from_api' + """ + + mapping: dict[str, type] + """Mapping of discriminator value to variant type, e.g. + + {'foo': FooVariant, 'bar': BarVariant} + """ + + def __init__( + self, + *, + mapping: dict[str, type], + discriminator_field: str, + discriminator_alias: str | None, + ) -> None: + self.mapping = mapping + self.field_name = discriminator_field + self.field_alias_from = discriminator_alias + + +def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, ...]) -> DiscriminatorDetails | None: + cached = DISCRIMINATOR_CACHE.get(union) + if cached is not None: + return cached + + discriminator_field_name: str | None = None + + for annotation in meta_annotations: + if isinstance(annotation, PropertyInfo) and annotation.discriminator is not None: + discriminator_field_name = annotation.discriminator + break + + if not discriminator_field_name: + return None + + mapping: dict[str, type] = {} + discriminator_alias: str | None = None + + for variant in get_args(union): + variant = strip_annotated_type(variant) + if is_basemodel_type(variant): + if PYDANTIC_V1: + field_info = cast("dict[str, FieldInfo]", variant.__fields__).get(discriminator_field_name) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] + if not field_info: + continue + + # Note: if one variant defines an alias then they all should + discriminator_alias = field_info.alias + + if (annotation := getattr(field_info, "annotation", None)) and is_literal_type(annotation): + for entry in get_args(annotation): + if isinstance(entry, str): + mapping[entry] = variant + else: + field = _extract_field_schema_pv2(variant, discriminator_field_name) + if not field: + continue + + # Note: if one variant defines an alias then they all should + discriminator_alias = field.get("serialization_alias") + + field_schema = field["schema"] + + if field_schema["type"] == "literal": + for entry in cast("LiteralSchema", field_schema)["expected"]: + if isinstance(entry, str): + mapping[entry] = variant + + if not mapping: + return None + + details = DiscriminatorDetails( + mapping=mapping, + discriminator_field=discriminator_field_name, + discriminator_alias=discriminator_alias, + ) + DISCRIMINATOR_CACHE.setdefault(union, details) + return details + + +def _extract_field_schema_pv2(model: type[BaseModel], field_name: str) -> ModelField | None: + schema = model.__pydantic_core_schema__ + if schema["type"] == "definitions": + schema = schema["schema"] + + if schema["type"] != "model": + return None + + schema = cast("ModelSchema", schema) + fields_schema = schema["schema"] + if fields_schema["type"] != "model-fields": + return None + + fields_schema = cast("ModelFieldsSchema", fields_schema) + field = fields_schema["fields"].get(field_name) + if not field: + return None + + return cast("ModelField", field) # pyright: ignore[reportUnnecessaryCast] + + +def validate_type(*, type_: type[_T], value: object) -> _T: + """Strict validation that the given value matches the expected type""" + if inspect.isclass(type_) and issubclass(type_, pydantic.BaseModel): + return cast(_T, parse_obj(type_, value)) + + return cast(_T, _validate_non_model_type(type_=type_, value=value)) + + +def set_pydantic_config(typ: Any, config: pydantic.ConfigDict) -> None: + """Add a pydantic config for the given type. + + Note: this is a no-op on Pydantic v1. + """ + setattr(typ, "__pydantic_config__", config) # noqa: B010 + + +# our use of subclassing here causes weirdness for type checkers, +# so we just pretend that we don't subclass +if TYPE_CHECKING: + GenericModel = BaseModel +else: + + class GenericModel(BaseGenericModel, BaseModel): + pass + + +if not PYDANTIC_V1: + from pydantic import TypeAdapter as _TypeAdapter + + _CachedTypeAdapter = cast("TypeAdapter[object]", lru_cache(maxsize=None)(_TypeAdapter)) + + if TYPE_CHECKING: + from pydantic import TypeAdapter + else: + TypeAdapter = _CachedTypeAdapter + + def _validate_non_model_type(*, type_: type[_T], value: object) -> _T: + return TypeAdapter(type_).validate_python(value) + +elif not TYPE_CHECKING: # TODO: condition is weird + + class RootModel(GenericModel, Generic[_T]): + """Used as a placeholder to easily convert runtime types to a Pydantic format + to provide validation. + + For example: + ```py + validated = RootModel[int](__root__="5").__root__ + # validated: 5 + ``` + """ + + __root__: _T + + def _validate_non_model_type(*, type_: type[_T], value: object) -> _T: + model = _create_pydantic_model(type_).validate(value) + return cast(_T, model.__root__) + + def _create_pydantic_model(type_: _T) -> Type[RootModel[_T]]: + return RootModel[type_] # type: ignore + + +class FinalRequestOptionsInput(TypedDict, total=False): + method: Required[str] + url: Required[str] + params: Query + headers: Headers + max_retries: int + timeout: float | Timeout | None + files: HttpxRequestFiles | None + idempotency_key: str + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] + json_data: Body + extra_json: AnyMapping + follow_redirects: bool + + +@final +class FinalRequestOptions(pydantic.BaseModel): + method: str + url: str + params: Query = {} + headers: Union[Headers, NotGiven] = NotGiven() + max_retries: Union[int, NotGiven] = NotGiven() + timeout: Union[float, Timeout, None, NotGiven] = NotGiven() + files: Union[HttpxRequestFiles, None] = None + idempotency_key: Union[str, None] = None + post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() + follow_redirects: Union[bool, None] = None + + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None + # It should be noted that we cannot use `json` here as that would override + # a BaseModel method in an incompatible fashion. + json_data: Union[Body, None] = None + extra_json: Union[AnyMapping, None] = None + + if PYDANTIC_V1: + + class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated] + arbitrary_types_allowed: bool = True + else: + model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True) + + def get_max_retries(self, max_retries: int) -> int: + if isinstance(self.max_retries, NotGiven): + return max_retries + return self.max_retries + + def _strip_raw_response_header(self) -> None: + if not is_given(self.headers): + return + + if self.headers.get(RAW_RESPONSE_HEADER): + self.headers = {**self.headers} + self.headers.pop(RAW_RESPONSE_HEADER) + + # override the `construct` method so that we can run custom transformations. + # this is necessary as we don't want to do any actual runtime type checking + # (which means we can't use validators) but we do want to ensure that `NotGiven` + # values are not present + # + # type ignore required because we're adding explicit types to `**values` + @classmethod + def construct( # type: ignore + cls, + _fields_set: set[str] | None = None, + **values: Unpack[FinalRequestOptionsInput], + ) -> FinalRequestOptions: + kwargs: dict[str, Any] = { + # we unconditionally call `strip_not_given` on any value + # as it will just ignore any non-mapping types + key: strip_not_given(value) + for key, value in values.items() + } + if PYDANTIC_V1: + return cast(FinalRequestOptions, super().construct(_fields_set, **kwargs)) # pyright: ignore[reportDeprecated] + return super().model_construct(_fields_set, **kwargs) + + if not TYPE_CHECKING: + # type checkers incorrectly complain about this assignment + model_construct = construct diff --git a/src/runloop_api_client/_qs.py b/src/runloop_api_client/_qs.py new file mode 100644 index 000000000..ada6fd3f7 --- /dev/null +++ b/src/runloop_api_client/_qs.py @@ -0,0 +1,150 @@ +from __future__ import annotations + +from typing import Any, List, Tuple, Union, Mapping, TypeVar +from urllib.parse import parse_qs, urlencode +from typing_extensions import Literal, get_args + +from ._types import NotGiven, not_given +from ._utils import flatten + +_T = TypeVar("_T") + + +ArrayFormat = Literal["comma", "repeat", "indices", "brackets"] +NestedFormat = Literal["dots", "brackets"] + +PrimitiveData = Union[str, int, float, bool, None] +# this should be Data = Union[PrimitiveData, "List[Data]", "Tuple[Data]", "Mapping[str, Data]"] +# https://github.com/microsoft/pyright/issues/3555 +Data = Union[PrimitiveData, List[Any], Tuple[Any], "Mapping[str, Any]"] +Params = Mapping[str, Data] + + +class Querystring: + array_format: ArrayFormat + nested_format: NestedFormat + + def __init__( + self, + *, + array_format: ArrayFormat = "repeat", + nested_format: NestedFormat = "brackets", + ) -> None: + self.array_format = array_format + self.nested_format = nested_format + + def parse(self, query: str) -> Mapping[str, object]: + # Note: custom format syntax is not supported yet + return parse_qs(query) + + def stringify( + self, + params: Params, + *, + array_format: ArrayFormat | NotGiven = not_given, + nested_format: NestedFormat | NotGiven = not_given, + ) -> str: + return urlencode( + self.stringify_items( + params, + array_format=array_format, + nested_format=nested_format, + ) + ) + + def stringify_items( + self, + params: Params, + *, + array_format: ArrayFormat | NotGiven = not_given, + nested_format: NestedFormat | NotGiven = not_given, + ) -> list[tuple[str, str]]: + opts = Options( + qs=self, + array_format=array_format, + nested_format=nested_format, + ) + return flatten([self._stringify_item(key, value, opts) for key, value in params.items()]) + + def _stringify_item( + self, + key: str, + value: Data, + opts: Options, + ) -> list[tuple[str, str]]: + if isinstance(value, Mapping): + items: list[tuple[str, str]] = [] + nested_format = opts.nested_format + for subkey, subvalue in value.items(): + items.extend( + self._stringify_item( + # TODO: error if unknown format + f"{key}.{subkey}" if nested_format == "dots" else f"{key}[{subkey}]", + subvalue, + opts, + ) + ) + return items + + if isinstance(value, (list, tuple)): + array_format = opts.array_format + if array_format == "comma": + return [ + ( + key, + ",".join(self._primitive_value_to_str(item) for item in value if item is not None), + ), + ] + elif array_format == "repeat": + items = [] + for item in value: + items.extend(self._stringify_item(key, item, opts)) + return items + elif array_format == "indices": + raise NotImplementedError("The array indices format is not supported yet") + elif array_format == "brackets": + items = [] + key = key + "[]" + for item in value: + items.extend(self._stringify_item(key, item, opts)) + return items + else: + raise NotImplementedError( + f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}" + ) + + serialised = self._primitive_value_to_str(value) + if not serialised: + return [] + return [(key, serialised)] + + def _primitive_value_to_str(self, value: PrimitiveData) -> str: + # copied from httpx + if value is True: + return "true" + elif value is False: + return "false" + elif value is None: + return "" + return str(value) + + +_qs = Querystring() +parse = _qs.parse +stringify = _qs.stringify +stringify_items = _qs.stringify_items + + +class Options: + array_format: ArrayFormat + nested_format: NestedFormat + + def __init__( + self, + qs: Querystring = _qs, + *, + array_format: ArrayFormat | NotGiven = not_given, + nested_format: NestedFormat | NotGiven = not_given, + ) -> None: + self.array_format = qs.array_format if isinstance(array_format, NotGiven) else array_format + self.nested_format = qs.nested_format if isinstance(nested_format, NotGiven) else nested_format diff --git a/src/runloop_api_client/_resource.py b/src/runloop_api_client/_resource.py new file mode 100644 index 000000000..bba835b09 --- /dev/null +++ b/src/runloop_api_client/_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 Runloop, AsyncRunloop + + +class SyncAPIResource: + _client: Runloop + + def __init__(self, client: Runloop) -> 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: AsyncRunloop + + def __init__(self, client: AsyncRunloop) -> 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/runloop_api_client/_response.py b/src/runloop_api_client/_response.py new file mode 100644 index 000000000..ddafda4fe --- /dev/null +++ b/src/runloop_api_client/_response.py @@ -0,0 +1,835 @@ +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 RunloopError, 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), + options=self._options, + ), + ) + + 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), + options=self._options, + ), + ) + + 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), + options=self._options, + ), + ) + + 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 runloop_api_client 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 runloop_api_client 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 runloop_api_client 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 `runloop_api_client._streaming` for reference", + ) + + +class StreamAlreadyConsumed(RunloopError): + """ + 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/runloop_api_client/_streaming.py b/src/runloop_api_client/_streaming.py new file mode 100644 index 000000000..16f910d25 --- /dev/null +++ b/src/runloop_api_client/_streaming.py @@ -0,0 +1,338 @@ +# 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, Optional, 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 Runloop, AsyncRunloop + from ._models import FinalRequestOptions + + +_T = TypeVar("_T") + + +class Stream(Generic[_T]): + """Provides the core interface to iterate over a synchronous stream response.""" + + response: httpx.Response + _options: Optional[FinalRequestOptions] = None + _decoder: SSEBytesDecoder + + def __init__( + self, + *, + cast_to: type[_T], + response: httpx.Response, + client: Runloop, + options: Optional[FinalRequestOptions] = None, + ) -> None: + self.response = response + self._cast_to = cast_to + self._client = client + self._options = options + self._decoder = client._make_sse_decoder() + self._iterator = self.__stream__() + + def __next__(self) -> _T: + return self._iterator.__next__() + + def __iter__(self) -> Iterator[_T]: + for item in self._iterator: + yield item + + def _iter_events(self) -> Iterator[ServerSentEvent]: + yield from self._decoder.iter_bytes(self.response.iter_bytes()) + + def __stream__(self) -> Iterator[_T]: + cast_to = cast(Any, self._cast_to) + response = self.response + process_data = self._client._process_response_data + iterator = self._iter_events() + + try: + for sse in iterator: + yield process_data(data=sse.json(), cast_to=cast_to, response=response) + finally: + # Ensure the response is closed even if the consumer doesn't read all data + response.close() + + def __enter__(self) -> Self: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.close() + + def close(self) -> None: + """ + Close the response and release the connection. + + Automatically called if the response body is read to completion. + """ + self.response.close() + + +class AsyncStream(Generic[_T]): + """Provides the core interface to iterate over an asynchronous stream response.""" + + response: httpx.Response + _options: Optional[FinalRequestOptions] = None + _decoder: SSEDecoder | SSEBytesDecoder + + def __init__( + self, + *, + cast_to: type[_T], + response: httpx.Response, + client: AsyncRunloop, + options: Optional[FinalRequestOptions] = None, + ) -> None: + self.response = response + self._cast_to = cast_to + self._client = client + self._options = options + self._decoder = client._make_sse_decoder() + self._iterator = self.__stream__() + + async def __anext__(self) -> _T: + return await self._iterator.__anext__() + + async def __aiter__(self) -> AsyncIterator[_T]: + async for item in self._iterator: + yield item + + async def _iter_events(self) -> AsyncIterator[ServerSentEvent]: + async for sse in self._decoder.aiter_bytes(self.response.aiter_bytes()): + yield sse + + async def __stream__(self) -> AsyncIterator[_T]: + cast_to = cast(Any, self._cast_to) + response = self.response + process_data = self._client._process_response_data + iterator = self._iter_events() + + try: + async for sse in iterator: + yield process_data(data=sse.json(), cast_to=cast_to, response=response) + finally: + # Ensure the response is closed even if the consumer doesn't read all data + await response.aclose() + + async def __aenter__(self) -> Self: + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + await self.close() + + async def close(self) -> None: + """ + Close the response and release the connection. + + Automatically called if the response body is read to completion. + """ + await self.response.aclose() + + +class ServerSentEvent: + def __init__( + self, + *, + event: str | None = None, + data: str | None = None, + id: str | None = None, + retry: int | None = None, + ) -> None: + if data is None: + data = "" + + self._id = id + self._data = data + self._event = event or None + self._retry = retry + + @property + def event(self) -> str | None: + return self._event + + @property + def id(self) -> str | None: + return self._id + + @property + def retry(self) -> int | None: + return self._retry + + @property + def data(self) -> str: + return self._data + + def json(self) -> Any: + return json.loads(self.data) + + @override + def __repr__(self) -> str: + return f"ServerSentEvent(event={self.event}, data={self.data}, id={self.id}, retry={self.retry})" + + +class SSEDecoder: + _data: list[str] + _event: str | None + _retry: int | None + _last_event_id: str | None + + def __init__(self) -> None: + self._event = None + self._data = [] + self._last_event_id = None + self._retry = None + + def iter_bytes(self, iterator: Iterator[bytes]) -> Iterator[ServerSentEvent]: + """Given an iterator that yields raw binary data, iterate over it & yield every event encountered""" + for chunk in self._iter_chunks(iterator): + # Split before decoding so splitlines() only uses \r and \n + for raw_line in chunk.splitlines(): + line = raw_line.decode("utf-8") + sse = self.decode(line) + if sse: + yield sse + + def _iter_chunks(self, iterator: Iterator[bytes]) -> Iterator[bytes]: + """Given an iterator that yields raw binary data, iterate over it and yield individual SSE chunks""" + data = b"" + for chunk in iterator: + for line in chunk.splitlines(keepends=True): + data += line + if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")): + yield data + data = b"" + if data: + yield data + + async def aiter_bytes(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[ServerSentEvent]: + """Given an iterator that yields raw binary data, iterate over it & yield every event encountered""" + async for chunk in self._aiter_chunks(iterator): + # Split before decoding so splitlines() only uses \r and \n + for raw_line in chunk.splitlines(): + line = raw_line.decode("utf-8") + sse = self.decode(line) + if sse: + yield sse + + async def _aiter_chunks(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[bytes]: + """Given an iterator that yields raw binary data, iterate over it and yield individual SSE chunks""" + data = b"" + async for chunk in iterator: + for line in chunk.splitlines(keepends=True): + data += line + if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")): + yield data + data = b"" + if data: + yield data + + def decode(self, line: str) -> ServerSentEvent | None: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = None + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None + + +@runtime_checkable +class SSEBytesDecoder(Protocol): + def iter_bytes(self, iterator: Iterator[bytes]) -> Iterator[ServerSentEvent]: + """Given an iterator that yields raw binary data, iterate over it & yield every event encountered""" + ... + + def aiter_bytes(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[ServerSentEvent]: + """Given an async iterator that yields raw binary data, iterate over it & yield every event encountered""" + ... + + +def is_stream_class_type(typ: type) -> TypeGuard[type[Stream[object]] | type[AsyncStream[object]]]: + """TypeGuard for determining whether or not the given type is a subclass of `Stream` / `AsyncStream`""" + origin = get_origin(typ) or typ + return inspect.isclass(origin) and issubclass(origin, (Stream, AsyncStream)) + + +def extract_stream_chunk_type( + stream_cls: type, + *, + failure_message: str | None = None, +) -> type: + """Given a type like `Stream[T]`, returns the generic type variable `T`. + + This also handles the case where a concrete subclass is given, e.g. + ```py + class MyStream(Stream[bytes]): + ... + + extract_stream_chunk_type(MyStream) -> bytes + ``` + """ + from ._base_client import Stream, AsyncStream + + return extract_type_var_from_base( + stream_cls, + index=0, + generic_bases=cast("tuple[type, ...]", (Stream, AsyncStream)), + failure_message=failure_message, + ) diff --git a/src/runloop_api_client/_types.py b/src/runloop_api_client/_types.py new file mode 100644 index 000000000..9f7d6eb21 --- /dev/null +++ b/src/runloop_api_client/_types.py @@ -0,0 +1,270 @@ +from __future__ import annotations + +from os import PathLike +from typing import ( + IO, + TYPE_CHECKING, + Any, + Dict, + List, + Type, + Tuple, + Union, + Mapping, + TypeVar, + Callable, + Iterable, + Iterator, + Optional, + Sequence, + AsyncIterable, +) +from typing_extensions import ( + Set, + Literal, + Protocol, + TypeAlias, + TypedDict, + SupportsIndex, + overload, + override, + runtime_checkable, +) + +import httpx +import pydantic +from httpx import URL, Proxy, Timeout, Response, BaseTransport, AsyncBaseTransport + +if TYPE_CHECKING: + from ._models import BaseModel + from ._response import APIResponse, AsyncAPIResponse + +Transport = BaseTransport +AsyncTransport = AsyncBaseTransport +Query = Mapping[str, object] +Body = object +AnyMapping = Mapping[str, object] +ModelT = TypeVar("ModelT", bound=pydantic.BaseModel) +_T = TypeVar("_T") + + +# Approximates httpx internal ProxiesTypes and RequestFiles types +# while adding support for `PathLike` instances +ProxiesDict = Dict["str | URL", Union[None, str, URL, Proxy]] +ProxiesTypes = Union[str, Proxy, ProxiesDict] +if TYPE_CHECKING: + Base64FileInput = Union[IO[bytes], PathLike[str]] + FileContent = Union[IO[bytes], bytes, PathLike[str]] +else: + Base64FileInput = Union[IO[bytes], PathLike] + FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8. + + +# Used for sending raw binary data / streaming data in request bodies +# e.g. for file uploads without multipart encoding +BinaryTypes = Union[bytes, bytearray, IO[bytes], Iterable[bytes]] +AsyncBinaryTypes = Union[bytes, bytearray, IO[bytes], AsyncIterable[bytes]] + +FileTypes = Union[ + # file (or bytes) + FileContent, + # (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 runloop_api_client import NoneType +# client.get('/foo', cast_to=NoneType) +# +# or build it yourself: +# +# client.get('/foo', cast_to=type(None)) +if TYPE_CHECKING: + NoneType: Type[None] +else: + NoneType = type(None) + + +class RequestOptions(TypedDict, total=False): + headers: Headers + max_retries: int + timeout: float | Timeout | None + params: Query + extra_json: AnyMapping + idempotency_key: str + follow_redirects: bool + + +# Sentinel class used until PEP 0661 is accepted +class NotGiven: + """ + For parameters with a meaningful None value, we need to distinguish between + the user explicitly passing None, and the user not passing the parameter at + all. + + User code shouldn't need to use not_given directly. + + For example: + + ```py + def create(timeout: Timeout | None | NotGiven = not_given): ... + + + create(timeout=1) # 1s timeout + create(timeout=None) # No timeout + create() # Default timeout behavior + ``` + """ + + def __bool__(self) -> Literal[False]: + return False + + @override + def __repr__(self) -> str: + return "NOT_GIVEN" + + +not_given = NotGiven() +# for backwards compatibility: +NOT_GIVEN = NotGiven() + + +class Omit: + """ + To explicitly omit something from being sent in a request, use `omit`. + + ```py + # as the default `Content-Type` header is `application/json` that will be sent + client.post("/upload/files", files={"file": b"my raw file content"}) + + # you can't explicitly override the header as it has to be dynamically generated + # to look something like: 'multipart/form-data; boundary=0d8382fcf5f8c3be01ca2e11002d2983' + client.post(..., headers={"Content-Type": "multipart/form-data"}) + + # instead you can remove the default `application/json` header by passing omit + client.post(..., headers={"Content-Type": omit}) + ``` + """ + + def __bool__(self) -> Literal[False]: + return False + + +omit = Omit() + + +@runtime_checkable +class ModelBuilderProtocol(Protocol): + @classmethod + def build( + cls: type[_T], + *, + response: Response, + data: object, + ) -> _T: ... + + +Headers = Mapping[str, Union[str, Omit]] + + +class HeadersLikeProtocol(Protocol): + def get(self, __key: str) -> str | None: ... + + +HeadersLike = Union[Headers, HeadersLikeProtocol] + +ResponseT = TypeVar( + "ResponseT", + bound=Union[ + object, + str, + None, + "BaseModel", + List[Any], + Dict[str, Any], + Response, + ModelBuilderProtocol, + "APIResponse[Any]", + "AsyncAPIResponse[Any]", + ], +) + +StrBytesIntFloat = Union[str, bytes, int, float] + +# Note: copied from Pydantic +# https://github.com/pydantic/pydantic/blob/6f31f8f68ef011f84357330186f603ff295312fd/pydantic/main.py#L79 +IncEx: TypeAlias = Union[Set[int], Set[str], Mapping[int, Union["IncEx", bool]], Mapping[str, Union["IncEx", bool]]] + +PostParser = Callable[[Any], Any] + + +@runtime_checkable +class InheritsGeneric(Protocol): + """Represents a type that has inherited from `Generic` + + The `__orig_bases__` property can be used to determine the resolved + type variable for a given base class. + """ + + __orig_bases__: tuple[_GenericAlias] + + +class _GenericAlias(Protocol): + __origin__: type[object] + + +class HttpxSendArgs(TypedDict, total=False): + auth: httpx.Auth + follow_redirects: bool + + +_T_co = TypeVar("_T_co", covariant=True) + + +if TYPE_CHECKING: + # This works because str.__contains__ does not accept object (either in typeshed or at runtime) + # https://github.com/hauntsaninja/useful_types/blob/5e9710f3875107d068e7679fd7fec9cfab0eff3b/useful_types/__init__.py#L285 + # + # Note: index() and count() methods are intentionally omitted to allow pyright to properly + # infer TypedDict types when dict literals are used in lists assigned to SequenceNotStr. + class SequenceNotStr(Protocol[_T_co]): + @overload + def __getitem__(self, index: SupportsIndex, /) -> _T_co: ... + @overload + def __getitem__(self, index: slice, /) -> Sequence[_T_co]: ... + def __contains__(self, value: object, /) -> bool: ... + def __len__(self) -> int: ... + def __iter__(self) -> Iterator[_T_co]: ... + def __reversed__(self) -> Iterator[_T_co]: ... +else: + # just point this to a normal `Sequence` at runtime to avoid having to special case + # deserializing our custom sequence type + SequenceNotStr = Sequence diff --git a/src/runloop_api_client/_utils/__init__.py b/src/runloop_api_client/_utils/__init__.py new file mode 100644 index 000000000..dc64e29a1 --- /dev/null +++ b/src/runloop_api_client/_utils/__init__.py @@ -0,0 +1,64 @@ +from ._sync import asyncify as asyncify +from ._proxy import LazyProxy as LazyProxy +from ._utils import ( + flatten as flatten, + is_dict as is_dict, + is_list as is_list, + is_given as is_given, + is_tuple as is_tuple, + json_safe as json_safe, + lru_cache as lru_cache, + is_mapping as is_mapping, + is_tuple_t as is_tuple_t, + is_iterable as is_iterable, + is_sequence as is_sequence, + coerce_float as coerce_float, + is_mapping_t as is_mapping_t, + removeprefix as removeprefix, + removesuffix as removesuffix, + extract_files as extract_files, + is_sequence_t as is_sequence_t, + required_args as required_args, + coerce_boolean as coerce_boolean, + coerce_integer as coerce_integer, + file_from_path as file_from_path, + strip_not_given as strip_not_given, + deepcopy_minimal as deepcopy_minimal, + get_async_library as get_async_library, + maybe_coerce_float as maybe_coerce_float, + get_required_header as get_required_header, + maybe_coerce_boolean as maybe_coerce_boolean, + maybe_coerce_integer as maybe_coerce_integer, +) +from ._compat import ( + get_args as get_args, + is_union as is_union, + get_origin as get_origin, + is_typeddict as is_typeddict, + is_literal_type as is_literal_type, +) +from ._typing import ( + is_list_type as is_list_type, + is_union_type as is_union_type, + extract_type_arg as extract_type_arg, + is_iterable_type as is_iterable_type, + is_required_type as is_required_type, + is_sequence_type as is_sequence_type, + is_annotated_type as is_annotated_type, + is_type_alias_type as is_type_alias_type, + strip_annotated_type as strip_annotated_type, + extract_type_var_from_base as extract_type_var_from_base, +) +from ._streams import consume_sync_iterator as consume_sync_iterator, consume_async_iterator as consume_async_iterator +from ._transform import ( + PropertyInfo as PropertyInfo, + transform as transform, + async_transform as async_transform, + maybe_transform as maybe_transform, + async_maybe_transform as async_maybe_transform, +) +from ._reflection import ( + function_has_argument as function_has_argument, + assert_signatures_in_sync as assert_signatures_in_sync, +) +from ._datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime diff --git a/src/runloop_api_client/_utils/_compat.py b/src/runloop_api_client/_utils/_compat.py new file mode 100644 index 000000000..2c70b299c --- /dev/null +++ b/src/runloop_api_client/_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 # type: ignore[comparison-overlap] + + +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/runloop_api_client/_utils/_datetime_parse.py b/src/runloop_api_client/_utils/_datetime_parse.py new file mode 100644 index 000000000..7cb9d9e66 --- /dev/null +++ b/src/runloop_api_client/_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/runloop_api_client/_utils/_json.py b/src/runloop_api_client/_utils/_json.py new file mode 100644 index 000000000..60584214a --- /dev/null +++ b/src/runloop_api_client/_utils/_json.py @@ -0,0 +1,35 @@ +import json +from typing import Any +from datetime import datetime +from typing_extensions import override + +import pydantic + +from .._compat import model_dump + + +def openapi_dumps(obj: Any) -> bytes: + """ + Serialize an object to UTF-8 encoded JSON bytes. + + Extends the standard json.dumps with support for additional types + commonly used in the SDK, such as `datetime`, `pydantic.BaseModel`, etc. + """ + return json.dumps( + obj, + cls=_CustomEncoder, + # Uses the same defaults as httpx's JSON serialization + ensure_ascii=False, + separators=(",", ":"), + allow_nan=False, + ).encode() + + +class _CustomEncoder(json.JSONEncoder): + @override + def default(self, o: Any) -> Any: + if isinstance(o, datetime): + return o.isoformat() + if isinstance(o, pydantic.BaseModel): + return model_dump(o, exclude_unset=True, mode="json", by_alias=True) + return super().default(o) diff --git a/src/runloop_api_client/_utils/_logs.py b/src/runloop_api_client/_utils/_logs.py new file mode 100644 index 000000000..b165781b6 --- /dev/null +++ b/src/runloop_api_client/_utils/_logs.py @@ -0,0 +1,25 @@ +import os +import logging + +logger: logging.Logger = logging.getLogger("runloop_api_client") +httpx_logger: logging.Logger = logging.getLogger("httpx") + + +def _basic_config() -> None: + # e.g. [2023-10-05 14:12:26 - runloop_api_client._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("RUNLOOP_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/runloop_api_client/_utils/_proxy.py b/src/runloop_api_client/_utils/_proxy.py new file mode 100644 index 000000000..0f239a33c --- /dev/null +++ b/src/runloop_api_client/_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/runloop_api_client/_utils/_reflection.py b/src/runloop_api_client/_utils/_reflection.py new file mode 100644 index 000000000..89aa712ac --- /dev/null +++ b/src/runloop_api_client/_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/runloop_api_client/_utils/_resources_proxy.py b/src/runloop_api_client/_utils/_resources_proxy.py new file mode 100644 index 000000000..a3b5c0479 --- /dev/null +++ b/src/runloop_api_client/_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 `runloop_api_client.resources` module. + + This is used so that we can lazily import `runloop_api_client.resources` only when + needed *and* so that users can just import `runloop_api_client` and reference `runloop_api_client.resources` + """ + + @override + def __load__(self) -> Any: + import importlib + + mod = importlib.import_module("runloop_api_client.resources") + return mod + + +resources = ResourcesProxy().__as_proxied__() diff --git a/src/runloop_api_client/_utils/_streams.py b/src/runloop_api_client/_utils/_streams.py new file mode 100644 index 000000000..f4a0208f0 --- /dev/null +++ b/src/runloop_api_client/_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/runloop_api_client/_utils/_sync.py b/src/runloop_api_client/_utils/_sync.py new file mode 100644 index 000000000..f6027c183 --- /dev/null +++ b/src/runloop_api_client/_utils/_sync.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +import asyncio +import functools +from typing import TypeVar, Callable, Awaitable +from typing_extensions import ParamSpec + +import anyio +import sniffio +import anyio.to_thread + +T_Retval = TypeVar("T_Retval") +T_ParamSpec = ParamSpec("T_ParamSpec") + + +async def to_thread( + func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs +) -> T_Retval: + if sniffio.current_async_library() == "asyncio": + return await asyncio.to_thread(func, *args, **kwargs) + + return await anyio.to_thread.run_sync( + functools.partial(func, *args, **kwargs), + ) + + +# inspired by `asyncer`, https://github.com/tiangolo/asyncer +def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]: + """ + Take a blocking function and create an async one that receives the same + positional and keyword arguments. + + Usage: + + ```python + def blocking_func(arg1, arg2, kwarg1=None): + # blocking code + return result + + + result = asyncify(blocking_function)(arg1, arg2, kwarg1=value1) + ``` + + ## Arguments + + `function`: a blocking regular callable (e.g. a function) + + ## Return + + An async function that takes the same positional and keyword arguments as the + original one, that when called runs the same original function in a thread worker + and returns the result. + """ + + async def wrapper(*args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs) -> T_Retval: + return await to_thread(function, *args, **kwargs) + + return wrapper diff --git a/src/runloop_api_client/_utils/_transform.py b/src/runloop_api_client/_utils/_transform.py new file mode 100644 index 000000000..520754920 --- /dev/null +++ b/src/runloop_api_client/_utils/_transform.py @@ -0,0 +1,457 @@ +from __future__ import annotations + +import io +import base64 +import pathlib +from typing import Any, Mapping, TypeVar, cast +from datetime import date, datetime +from typing_extensions import Literal, get_args, override, get_type_hints as _get_type_hints + +import anyio +import pydantic + +from ._utils import ( + is_list, + is_given, + lru_cache, + is_mapping, + is_iterable, + is_sequence, +) +from .._files import is_base64_file_input +from ._compat import get_origin, is_typeddict +from ._typing import ( + is_list_type, + is_union_type, + extract_type_arg, + is_iterable_type, + is_required_type, + is_sequence_type, + is_annotated_type, + strip_annotated_type, +) + +_T = TypeVar("_T") + + +# TODO: support for drilling globals() and locals() +# TODO: ensure works correctly with forward references in all cases + + +PropertyFormat = Literal["iso8601", "base64", "custom"] + + +class PropertyInfo: + """Metadata class to be used in Annotated types to provide information about a given type. + + For example: + + class MyParams(TypedDict): + account_holder_name: Annotated[str, PropertyInfo(alias='accountHolderName')] + + This means that {'account_holder_name': 'Robert'} will be transformed to {'accountHolderName': 'Robert'} before being sent to the API. + """ + + alias: str | None + format: PropertyFormat | None + format_template: str | None + discriminator: str | None + + def __init__( + self, + *, + alias: str | None = None, + format: PropertyFormat | None = None, + format_template: str | None = None, + discriminator: str | None = None, + ) -> None: + self.alias = alias + self.format = format + self.format_template = format_template + self.discriminator = discriminator + + @override + def __repr__(self) -> str: + return f"{self.__class__.__name__}(alias='{self.alias}', format={self.format}, format_template='{self.format_template}', discriminator='{self.discriminator}')" + + +def maybe_transform( + data: object, + expected_type: object, +) -> Any | None: + """Wrapper over `transform()` that allows `None` to be passed. + + See `transform()` for more details. + """ + if data is None: + return None + return transform(data, expected_type) + + +# Wrapper over _transform_recursive providing fake types +def transform( + data: _T, + expected_type: object, +) -> _T: + """Transform dictionaries based off of type information from the given type, for example: + + ```py + class Params(TypedDict, total=False): + card_id: Required[Annotated[str, PropertyInfo(alias="cardID")]] + + + transformed = transform({"card_id": ""}, Params) + # {'cardID': ''} + ``` + + Any keys / data that does not have type information given will be included as is. + + It should be noted that the transformations that this function does are not represented in the type system. + """ + transformed = _transform_recursive(data, annotation=cast(type, expected_type)) + return cast(_T, transformed) + + +@lru_cache(maxsize=8096) +def _get_annotated_type(type_: type) -> type | None: + """If the given type is an `Annotated` type then it is returned, if not `None` is returned. + + This also unwraps the type when applicable, e.g. `Required[Annotated[T, ...]]` + """ + if is_required_type(type_): + # Unwrap `Required[Annotated[T, ...]]` to `Annotated[T, ...]` + type_ = get_args(type_)[0] + + if is_annotated_type(type_): + return type_ + + return None + + +def _maybe_transform_key(key: str, type_: type) -> str: + """Transform the given `data` based on the annotations provided in `type_`. + + Note: this function only looks at `Annotated` types that contain `PropertyInfo` metadata. + """ + annotated_type = _get_annotated_type(type_) + if annotated_type is None: + # no `Annotated` definition for this type, no transformation needed + return key + + # ignore the first argument as it is the actual type + annotations = get_args(annotated_type)[1:] + for annotation in annotations: + if isinstance(annotation, PropertyInfo) and annotation.alias is not None: + return annotation.alias + + return key + + +def _no_transform_needed(annotation: type) -> bool: + return annotation == float or annotation == int + + +def _transform_recursive( + data: object, + *, + annotation: type, + inner_type: type | None = None, +) -> object: + """Transform the given data against the expected type. + + Args: + annotation: The direct type annotation given to the particular piece of data. + This may or may not be wrapped in metadata types, e.g. `Required[T]`, `Annotated[T, ...]` etc + + inner_type: If applicable, this is the "inside" type. This is useful in certain cases where the outside type + is a container type such as `List[T]`. In that case `inner_type` should be set to `T` so that each entry in + the list can be transformed using the metadata from the container type. + + Defaults to the same value as the `annotation` argument. + """ + from .._compat import model_dump + + if inner_type is None: + inner_type = annotation + + stripped_type = strip_annotated_type(inner_type) + origin = get_origin(stripped_type) or stripped_type + if is_typeddict(stripped_type) and is_mapping(data): + return _transform_typeddict(data, stripped_type) + + if origin == dict and is_mapping(data): + items_type = get_args(stripped_type)[1] + return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()} + + if ( + # List[T] + (is_list_type(stripped_type) and is_list(data)) + # Iterable[T] + or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str)) + # Sequence[T] + or (is_sequence_type(stripped_type) and is_sequence(data) and not isinstance(data, str)) + ): + # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually + # intended as an iterable, so we don't transform it. + if isinstance(data, dict): + return cast(object, data) + + inner_type = extract_type_arg(stripped_type, 0) + if _no_transform_needed(inner_type): + # for some types there is no need to transform anything, so we can get a small + # perf boost from skipping that work. + # + # but we still need to convert to a list to ensure the data is json-serializable + if is_list(data): + return data + return list(data) + + return [_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] + + if is_union_type(stripped_type): + # For union types we run the transformation against all subtypes to ensure that everything is transformed. + # + # TODO: there may be edge cases where the same normalized field name will transform to two different names + # in different subtypes. + for subtype in get_args(stripped_type): + data = _transform_recursive(data, annotation=annotation, inner_type=subtype) + return data + + if isinstance(data, pydantic.BaseModel): + return model_dump(data, exclude_unset=True, mode="json") + + annotated_type = _get_annotated_type(annotation) + if annotated_type is None: + return data + + # ignore the first argument as it is the actual type + annotations = get_args(annotated_type)[1:] + for annotation in annotations: + if isinstance(annotation, PropertyInfo) and annotation.format is not None: + return _format_data(data, annotation.format, annotation.format_template) + + return data + + +def _format_data(data: object, format_: PropertyFormat, format_template: str | None) -> object: + if isinstance(data, (date, datetime)): + if format_ == "iso8601": + return data.isoformat() + + if format_ == "custom" and format_template is not None: + return data.strftime(format_template) + + if format_ == "base64" and is_base64_file_input(data): + binary: str | bytes | None = None + + if isinstance(data, pathlib.Path): + binary = data.read_bytes() + elif isinstance(data, io.IOBase): + binary = data.read() + + if isinstance(binary, str): # type: ignore[unreachable] + binary = binary.encode() + + if not isinstance(binary, bytes): + raise RuntimeError(f"Could not read bytes from {data}; Received {type(binary)}") + + return base64.b64encode(binary).decode("ascii") + + return data + + +def _transform_typeddict( + data: Mapping[str, object], + expected_type: type, +) -> Mapping[str, object]: + result: dict[str, object] = {} + annotations = get_type_hints(expected_type, include_extras=True) + for key, value in data.items(): + if not is_given(value): + # we don't need to include omitted values here as they'll + # be stripped out before the request is sent anyway + continue + + type_ = annotations.get(key) + if type_ is None: + # we do not have a type annotation for this field, leave it as is + result[key] = value + else: + result[_maybe_transform_key(key, type_)] = _transform_recursive(value, annotation=type_) + return result + + +async def async_maybe_transform( + data: object, + expected_type: object, +) -> Any | None: + """Wrapper over `async_transform()` that allows `None` to be passed. + + See `async_transform()` for more details. + """ + if data is None: + return None + return await async_transform(data, expected_type) + + +async def async_transform( + data: _T, + expected_type: object, +) -> _T: + """Transform dictionaries based off of type information from the given type, for example: + + ```py + class Params(TypedDict, total=False): + card_id: Required[Annotated[str, PropertyInfo(alias="cardID")]] + + + transformed = transform({"card_id": ""}, Params) + # {'cardID': ''} + ``` + + Any keys / data that does not have type information given will be included as is. + + It should be noted that the transformations that this function does are not represented in the type system. + """ + transformed = await _async_transform_recursive(data, annotation=cast(type, expected_type)) + return cast(_T, transformed) + + +async def _async_transform_recursive( + data: object, + *, + annotation: type, + inner_type: type | None = None, +) -> object: + """Transform the given data against the expected type. + + Args: + annotation: The direct type annotation given to the particular piece of data. + This may or may not be wrapped in metadata types, e.g. `Required[T]`, `Annotated[T, ...]` etc + + inner_type: If applicable, this is the "inside" type. This is useful in certain cases where the outside type + is a container type such as `List[T]`. In that case `inner_type` should be set to `T` so that each entry in + the list can be transformed using the metadata from the container type. + + Defaults to the same value as the `annotation` argument. + """ + from .._compat import model_dump + + if inner_type is None: + inner_type = annotation + + stripped_type = strip_annotated_type(inner_type) + origin = get_origin(stripped_type) or stripped_type + if is_typeddict(stripped_type) and is_mapping(data): + return await _async_transform_typeddict(data, stripped_type) + + if origin == dict and is_mapping(data): + items_type = get_args(stripped_type)[1] + return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()} + + if ( + # List[T] + (is_list_type(stripped_type) and is_list(data)) + # Iterable[T] + or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str)) + # Sequence[T] + or (is_sequence_type(stripped_type) and is_sequence(data) and not isinstance(data, str)) + ): + # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually + # intended as an iterable, so we don't transform it. + if isinstance(data, dict): + return cast(object, data) + + inner_type = extract_type_arg(stripped_type, 0) + if _no_transform_needed(inner_type): + # for some types there is no need to transform anything, so we can get a small + # perf boost from skipping that work. + # + # but we still need to convert to a list to ensure the data is json-serializable + if is_list(data): + return data + return list(data) + + return [await _async_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] + + if is_union_type(stripped_type): + # For union types we run the transformation against all subtypes to ensure that everything is transformed. + # + # TODO: there may be edge cases where the same normalized field name will transform to two different names + # in different subtypes. + for subtype in get_args(stripped_type): + data = await _async_transform_recursive(data, annotation=annotation, inner_type=subtype) + return data + + if isinstance(data, pydantic.BaseModel): + return model_dump(data, exclude_unset=True, mode="json") + + annotated_type = _get_annotated_type(annotation) + if annotated_type is None: + return data + + # ignore the first argument as it is the actual type + annotations = get_args(annotated_type)[1:] + for annotation in annotations: + if isinstance(annotation, PropertyInfo) and annotation.format is not None: + return await _async_format_data(data, annotation.format, annotation.format_template) + + return data + + +async def _async_format_data(data: object, format_: PropertyFormat, format_template: str | None) -> object: + if isinstance(data, (date, datetime)): + if format_ == "iso8601": + return data.isoformat() + + if format_ == "custom" and format_template is not None: + return data.strftime(format_template) + + if format_ == "base64" and is_base64_file_input(data): + binary: str | bytes | None = None + + if isinstance(data, pathlib.Path): + binary = await anyio.Path(data).read_bytes() + elif isinstance(data, io.IOBase): + binary = data.read() + + if isinstance(binary, str): # type: ignore[unreachable] + binary = binary.encode() + + if not isinstance(binary, bytes): + raise RuntimeError(f"Could not read bytes from {data}; Received {type(binary)}") + + return base64.b64encode(binary).decode("ascii") + + return data + + +async def _async_transform_typeddict( + data: Mapping[str, object], + expected_type: type, +) -> Mapping[str, object]: + result: dict[str, object] = {} + annotations = get_type_hints(expected_type, include_extras=True) + for key, value in data.items(): + if not is_given(value): + # we don't need to include omitted values here as they'll + # be stripped out before the request is sent anyway + continue + + type_ = annotations.get(key) + if type_ is None: + # we do not have a type annotation for this field, leave it as is + result[key] = value + else: + result[_maybe_transform_key(key, type_)] = await _async_transform_recursive(value, annotation=type_) + return result + + +@lru_cache(maxsize=8096) +def get_type_hints( + obj: Any, + globalns: dict[str, Any] | None = None, + localns: Mapping[str, Any] | None = None, + include_extras: bool = False, +) -> dict[str, Any]: + return _get_type_hints(obj, globalns=globalns, localns=localns, include_extras=include_extras) diff --git a/src/runloop_api_client/_utils/_typing.py b/src/runloop_api_client/_utils/_typing.py new file mode 100644 index 000000000..193109f3a --- /dev/null +++ b/src/runloop_api_client/_utils/_typing.py @@ -0,0 +1,156 @@ +from __future__ import annotations + +import sys +import typing +import typing_extensions +from typing import Any, TypeVar, Iterable, cast +from collections import abc as _c_abc +from typing_extensions import ( + TypeIs, + Required, + Annotated, + get_args, + get_origin, +) + +from ._utils import lru_cache +from .._types import InheritsGeneric +from ._compat import is_union as _is_union + + +def is_annotated_type(typ: type) -> bool: + return get_origin(typ) == Annotated + + +def is_list_type(typ: type) -> bool: + return (get_origin(typ) or typ) == list + + +def is_sequence_type(typ: type) -> bool: + origin = get_origin(typ) or typ + return origin == typing_extensions.Sequence or origin == typing.Sequence or origin == _c_abc.Sequence + + +def is_iterable_type(typ: type) -> bool: + """If the given type is `typing.Iterable[T]`""" + origin = get_origin(typ) or typ + return origin == Iterable or origin == _c_abc.Iterable + + +def is_union_type(typ: type) -> bool: + return _is_union(get_origin(typ)) + + +def is_required_type(typ: type) -> bool: + return get_origin(typ) == Required + + +def is_typevar(typ: type) -> bool: + # type ignore is required because type checkers + # think this expression will always return False + return type(typ) == TypeVar # type: ignore + + +_TYPE_ALIAS_TYPES: tuple[type[typing_extensions.TypeAliasType], ...] = (typing_extensions.TypeAliasType,) +if sys.version_info >= (3, 12): + _TYPE_ALIAS_TYPES = (*_TYPE_ALIAS_TYPES, typing.TypeAliasType) + + +def is_type_alias_type(tp: Any, /) -> TypeIs[typing_extensions.TypeAliasType]: + """Return whether the provided argument is an instance of `TypeAliasType`. + + ```python + type Int = int + is_type_alias_type(Int) + # > True + Str = TypeAliasType("Str", str) + is_type_alias_type(Str) + # > True + ``` + """ + return isinstance(tp, _TYPE_ALIAS_TYPES) + + +# Extracts T from Annotated[T, ...] or from Required[Annotated[T, ...]] +@lru_cache(maxsize=8096) +def strip_annotated_type(typ: type) -> type: + if is_required_type(typ) or is_annotated_type(typ): + return strip_annotated_type(cast(type, get_args(typ)[0])) + + return typ + + +def extract_type_arg(typ: type, index: int) -> type: + args = get_args(typ) + try: + return cast(type, args[index]) + except IndexError as err: + raise RuntimeError(f"Expected type {typ} to have a type argument at index {index} but it did not") from err + + +def extract_type_var_from_base( + typ: type, + *, + generic_bases: tuple[type, ...], + index: int, + failure_message: str | None = None, +) -> type: + """Given a type like `Foo[T]`, returns the generic type variable `T`. + + This also handles the case where a concrete subclass is given, e.g. + ```py + class MyResponse(Foo[bytes]): + ... + + extract_type_var(MyResponse, bases=(Foo,), index=0) -> bytes + ``` + + And where a generic subclass is given: + ```py + _T = TypeVar('_T') + class MyResponse(Foo[_T]): + ... + + extract_type_var(MyResponse[bytes], bases=(Foo,), index=0) -> bytes + ``` + """ + cls = cast(object, get_origin(typ) or typ) + if cls in generic_bases: # pyright: ignore[reportUnnecessaryContains] + # we're given the class directly + return extract_type_arg(typ, index) + + # if a subclass is given + # --- + # this is needed as __orig_bases__ is not present in the typeshed stubs + # because it is intended to be for internal use only, however there does + # not seem to be a way to resolve generic TypeVars for inherited subclasses + # without using it. + if isinstance(cls, InheritsGeneric): + target_base_class: Any | None = None + for base in cls.__orig_bases__: + if base.__origin__ in generic_bases: + target_base_class = base + break + + if target_base_class is None: + raise RuntimeError( + "Could not find the generic base class;\n" + "This should never happen;\n" + f"Does {cls} inherit from one of {generic_bases} ?" + ) + + extracted = extract_type_arg(target_base_class, index) + if is_typevar(extracted): + # If the extracted type argument is itself a type variable + # then that means the subclass itself is generic, so we have + # to resolve the type argument from the class itself, not + # the base class. + # + # Note: if there is more than 1 type argument, the subclass could + # change the ordering of the type arguments, this is not currently + # supported. + return extract_type_arg(typ, index) + + return extracted + + raise RuntimeError(failure_message or f"Could not resolve inner type variable at index {index} for {typ}") diff --git a/src/runloop_api_client/_utils/_utils.py b/src/runloop_api_client/_utils/_utils.py new file mode 100644 index 000000000..eec7f4a1f --- /dev/null +++ b/src/runloop_api_client/_utils/_utils.py @@ -0,0 +1,421 @@ +from __future__ import annotations + +import os +import re +import inspect +import functools +from typing import ( + Any, + Tuple, + Mapping, + TypeVar, + Callable, + Iterable, + Sequence, + cast, + overload, +) +from pathlib import Path +from datetime import date, datetime +from typing_extensions import TypeGuard + +import sniffio + +from .._types import Omit, NotGiven, FileTypes, HeadersLike + +_T = TypeVar("_T") +_TupleT = TypeVar("_TupleT", bound=Tuple[object, ...]) +_MappingT = TypeVar("_MappingT", bound=Mapping[str, object]) +_SequenceT = TypeVar("_SequenceT", bound=Sequence[object]) +CallableT = TypeVar("CallableT", bound=Callable[..., Any]) + + +def flatten(t: Iterable[Iterable[_T]]) -> list[_T]: + return [item for sublist in t for item in sublist] + + +def extract_files( + # TODO: this needs to take Dict but variance issues..... + # create protocol type ? + query: Mapping[str, object], + *, + paths: Sequence[Sequence[str]], +) -> list[tuple[str, FileTypes]]: + """Recursively extract files from the given dictionary based on specified paths. + + A path may look like this ['foo', 'files', '', 'data']. + + Note: this mutates the given dictionary. + """ + files: list[tuple[str, FileTypes]] = [] + for path in paths: + files.extend(_extract_items(query, path, index=0, flattened_key=None)) + return files + + +def _extract_items( + obj: object, + path: Sequence[str], + *, + index: int, + flattened_key: str | None, +) -> list[tuple[str, FileTypes]]: + try: + key = path[index] + except IndexError: + if not is_given(obj): + # no value was provided - we can safely ignore + return [] + + # cyclical import + from .._files import assert_is_file_content + + # We have exhausted the path, return the entry we found. + assert flattened_key is not None + + if is_list(obj): + files: list[tuple[str, FileTypes]] = [] + for entry in obj: + assert_is_file_content(entry, key=flattened_key + "[]" if flattened_key else "") + files.append((flattened_key + "[]", cast(FileTypes, entry))) + return files + + assert_is_file_content(obj, key=flattened_key) + return [(flattened_key, cast(FileTypes, obj))] + + index += 1 + if is_dict(obj): + try: + # We are at the last entry in the path so we must remove the field + if (len(path)) == index: + item = obj.pop(key) + else: + item = obj[key] + except KeyError: + # Key was not present in the dictionary, this is not indicative of an error + # as the given path may not point to a required field. We also do not want + # to enforce required fields as the API may differ from the spec in some cases. + return [] + if flattened_key is None: + flattened_key = key + else: + flattened_key += f"[{key}]" + return _extract_items( + item, + path, + index=index, + flattened_key=flattened_key, + ) + elif is_list(obj): + if key != "": + return [] + + return flatten( + [ + _extract_items( + item, + path, + index=index, + flattened_key=flattened_key + "[]" if flattened_key is not None else "[]", + ) + for item in obj + ] + ) + + # Something unexpected was passed, just ignore it. + return [] + + +def is_given(obj: _T | NotGiven | Omit) -> TypeGuard[_T]: + return not isinstance(obj, NotGiven) and not isinstance(obj, Omit) + + +# Type safe methods for narrowing types with TypeVars. +# The default narrowing for isinstance(obj, dict) is dict[unknown, unknown], +# however this cause Pyright to rightfully report errors. As we know we don't +# care about the contained types we can safely use `object` in its place. +# +# There are two separate functions defined, `is_*` and `is_*_t` for different use cases. +# `is_*` is for when you're dealing with an unknown input +# `is_*_t` is for when you're narrowing a known union type to a specific subset + + +def is_tuple(obj: object) -> TypeGuard[tuple[object, ...]]: + return isinstance(obj, tuple) + + +def is_tuple_t(obj: _TupleT | object) -> TypeGuard[_TupleT]: + return isinstance(obj, tuple) + + +def is_sequence(obj: object) -> TypeGuard[Sequence[object]]: + return isinstance(obj, Sequence) + + +def is_sequence_t(obj: _SequenceT | object) -> TypeGuard[_SequenceT]: + return isinstance(obj, Sequence) + + +def is_mapping(obj: object) -> TypeGuard[Mapping[str, object]]: + return isinstance(obj, Mapping) + + +def is_mapping_t(obj: _MappingT | object) -> TypeGuard[_MappingT]: + return isinstance(obj, Mapping) + + +def is_dict(obj: object) -> TypeGuard[dict[object, object]]: + return isinstance(obj, dict) + + +def is_list(obj: object) -> TypeGuard[list[object]]: + return isinstance(obj, list) + + +def is_iterable(obj: object) -> TypeGuard[Iterable[object]]: + return isinstance(obj, Iterable) + + +def deepcopy_minimal(item: _T) -> _T: + """Minimal reimplementation of copy.deepcopy() that will only copy certain object types: + + - mappings, e.g. `dict` + - list + + This is done for performance reasons. + """ + if is_mapping(item): + return cast(_T, {k: deepcopy_minimal(v) for k, v in item.items()}) + if is_list(item): + return cast(_T, [deepcopy_minimal(entry) for entry in item]) + return item + + +# copied from https://github.com/Rapptz/RoboDanny +def human_join(seq: Sequence[str], *, delim: str = ", ", final: str = "or") -> str: + size = len(seq) + if size == 0: + return "" + + if size == 1: + return seq[0] + + if size == 2: + return f"{seq[0]} {final} {seq[1]}" + + return delim.join(seq[:-1]) + f" {final} {seq[-1]}" + + +def quote(string: str) -> str: + """Add single quotation marks around the given string. Does *not* do any escaping.""" + return f"'{string}'" + + +def required_args(*variants: Sequence[str]) -> Callable[[CallableT], CallableT]: + """Decorator to enforce a given set of arguments or variants of arguments are passed to the decorated function. + + Useful for enforcing runtime validation of overloaded functions. + + Example usage: + ```py + @overload + def foo(*, a: str) -> str: ... + + + @overload + def foo(*, b: bool) -> str: ... + + + # This enforces the same constraints that a static type checker would + # i.e. that either a or b must be passed to the function + @required_args(["a"], ["b"]) + def foo(*, a: str | None = None, b: bool | None = None) -> str: ... + ``` + """ + + def inner(func: CallableT) -> CallableT: + params = inspect.signature(func).parameters + positional = [ + name + for name, param in params.items() + if param.kind + in { + param.POSITIONAL_ONLY, + param.POSITIONAL_OR_KEYWORD, + } + ] + + @functools.wraps(func) + def wrapper(*args: object, **kwargs: object) -> object: + given_params: set[str] = set() + for i, _ in enumerate(args): + try: + given_params.add(positional[i]) + except IndexError: + raise TypeError( + f"{func.__name__}() takes {len(positional)} argument(s) but {len(args)} were given" + ) from None + + for key in kwargs.keys(): + given_params.add(key) + + for variant in variants: + matches = all((param in given_params for param in variant)) + if matches: + break + else: # no break + if len(variants) > 1: + variations = human_join( + ["(" + human_join([quote(arg) for arg in variant], final="and") + ")" for variant in variants] + ) + msg = f"Missing required arguments; Expected either {variations} arguments to be given" + else: + assert len(variants) > 0 + + # TODO: this error message is not deterministic + missing = list(set(variants[0]) - given_params) + if len(missing) > 1: + msg = f"Missing required arguments: {human_join([quote(arg) for arg in missing])}" + else: + msg = f"Missing required argument: {quote(missing[0])}" + raise TypeError(msg) + return func(*args, **kwargs) + + return wrapper # type: ignore + + return inner + + +_K = TypeVar("_K") +_V = TypeVar("_V") + + +@overload +def strip_not_given(obj: None) -> None: ... + + +@overload +def strip_not_given(obj: Mapping[_K, _V | NotGiven]) -> dict[_K, _V]: ... + + +@overload +def strip_not_given(obj: object) -> object: ... + + +def strip_not_given(obj: object | None) -> object: + """Remove all top-level keys where their values are instances of `NotGiven`""" + if obj is None: + return None + + if not is_mapping(obj): + return obj + + return {key: value for key, value in obj.items() if not isinstance(value, NotGiven)} + + +def coerce_integer(val: str) -> int: + return int(val, base=10) + + +def coerce_float(val: str) -> float: + return float(val) + + +def coerce_boolean(val: str) -> bool: + return val == "true" or val == "1" or val == "on" + + +def maybe_coerce_integer(val: str | None) -> int | None: + if val is None: + return None + return coerce_integer(val) + + +def maybe_coerce_float(val: str | None) -> float | None: + if val is None: + return None + return coerce_float(val) + + +def maybe_coerce_boolean(val: str | None) -> bool | None: + if val is None: + return None + return coerce_boolean(val) + + +def removeprefix(string: str, prefix: str) -> str: + """Remove a prefix from a string. + + Backport of `str.removeprefix` for Python < 3.9 + """ + if string.startswith(prefix): + return string[len(prefix) :] + return string + + +def removesuffix(string: str, suffix: str) -> str: + """Remove a suffix from a string. + + Backport of `str.removesuffix` for Python < 3.9 + """ + if string.endswith(suffix): + return string[: -len(suffix)] + return string + + +def file_from_path(path: str) -> FileTypes: + contents = Path(path).read_bytes() + file_name = os.path.basename(path) + return (file_name, contents) + + +def get_required_header(headers: HeadersLike, header: str) -> str: + lower_header = header.lower() + if is_mapping_t(headers): + # mypy doesn't understand the type narrowing here + for k, v in headers.items(): # type: ignore + if k.lower() == lower_header and isinstance(v, str): + return v + + # to deal with the case where the header looks like Stainless-Event-Id + intercaps_header = re.sub(r"([^\w])(\w)", lambda pat: pat.group(1) + pat.group(2).upper(), header.capitalize()) + + for normalized_header in [header, lower_header, header.upper(), intercaps_header]: + value = headers.get(normalized_header) + if value: + return value + + raise ValueError(f"Could not find {header} header") + + +def get_async_library() -> str: + try: + return sniffio.current_async_library() + except Exception: + return "false" + + +def lru_cache(*, maxsize: int | None = 128) -> Callable[[CallableT], CallableT]: + """A version of functools.lru_cache that retains the type signature + for the wrapped function arguments. + """ + wrapper = functools.lru_cache( # noqa: TID251 + maxsize=maxsize, + ) + return cast(Any, wrapper) # type: ignore[no-any-return] + + +def json_safe(data: object) -> object: + """Translates a mapping / sequence recursively in the same fashion + as `pydantic` v2's `model_dump(mode="json")`. + """ + if is_mapping(data): + return {json_safe(key): json_safe(value) for key, value in data.items()} + + if is_iterable(data) and not isinstance(data, (str, bytes, bytearray)): + return [json_safe(item) for item in data] + + if isinstance(data, (datetime, date)): + return data.isoformat() + + return data diff --git a/src/runloop_api_client/_version.py b/src/runloop_api_client/_version.py new file mode 100644 index 000000000..93214b609 --- /dev/null +++ b/src/runloop_api_client/_version.py @@ -0,0 +1,4 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +__title__ = "runloop_api_client" +__version__ = "1.11.0" # x-release-please-version diff --git a/src/runloop_api_client/lib/.keep b/src/runloop_api_client/lib/.keep new file mode 100644 index 000000000..5e2c99fdb --- /dev/null +++ b/src/runloop_api_client/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/runloop_api_client/pagination.py b/src/runloop_api_client/pagination.py new file mode 100644 index 000000000..e084ef68e --- /dev/null +++ b/src/runloop_api_client/pagination.py @@ -0,0 +1,1061 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Any, List, Generic, TypeVar, Optional, cast +from typing_extensions import Protocol, override, runtime_checkable + +from ._base_client import BasePage, PageInfo, BaseSyncPage, BaseAsyncPage + +__all__ = [ + "SyncBlueprintsCursorIDPage", + "AsyncBlueprintsCursorIDPage", + "SyncDevboxesCursorIDPage", + "AsyncDevboxesCursorIDPage", + "SyncRepositoriesCursorIDPage", + "AsyncRepositoriesCursorIDPage", + "SyncDiskSnapshotsCursorIDPage", + "AsyncDiskSnapshotsCursorIDPage", + "SyncBenchmarksCursorIDPage", + "AsyncBenchmarksCursorIDPage", + "SyncAgentsCursorIDPage", + "AsyncAgentsCursorIDPage", + "SyncBenchmarkRunsCursorIDPage", + "AsyncBenchmarkRunsCursorIDPage", + "SyncScenariosCursorIDPage", + "AsyncScenariosCursorIDPage", + "SyncScenarioRunsCursorIDPage", + "AsyncScenarioRunsCursorIDPage", + "SyncScenarioScorersCursorIDPage", + "AsyncScenarioScorersCursorIDPage", + "SyncObjectsCursorIDPage", + "AsyncObjectsCursorIDPage", + "SyncNetworkPoliciesCursorIDPage", + "AsyncNetworkPoliciesCursorIDPage", + "SyncGatewayConfigsCursorIDPage", + "AsyncGatewayConfigsCursorIDPage", + "SyncMcpConfigsCursorIDPage", + "AsyncMcpConfigsCursorIDPage", +] + +_T = TypeVar("_T") + + +@runtime_checkable +class BlueprintsCursorIDPageItem(Protocol): + id: str + + +@runtime_checkable +class DevboxesCursorIDPageItem(Protocol): + id: str + + +@runtime_checkable +class RepositoriesCursorIDPageItem(Protocol): + id: str + + +@runtime_checkable +class DiskSnapshotsCursorIDPageItem(Protocol): + id: str + + +@runtime_checkable +class BenchmarksCursorIDPageItem(Protocol): + id: str + + +@runtime_checkable +class AgentsCursorIDPageItem(Protocol): + id: str + + +@runtime_checkable +class BenchmarkRunsCursorIDPageItem(Protocol): + id: str + + +@runtime_checkable +class ScenariosCursorIDPageItem(Protocol): + id: str + + +@runtime_checkable +class ScenarioRunsCursorIDPageItem(Protocol): + id: str + + +@runtime_checkable +class ScenarioScorersCursorIDPageItem(Protocol): + id: str + + +@runtime_checkable +class ObjectsCursorIDPageItem(Protocol): + id: str + + +@runtime_checkable +class NetworkPoliciesCursorIDPageItem(Protocol): + id: str + + +@runtime_checkable +class GatewayConfigsCursorIDPageItem(Protocol): + id: str + + +@runtime_checkable +class McpConfigsCursorIDPageItem(Protocol): + id: str + + +class SyncBlueprintsCursorIDPage(BaseSyncPage[_T], BasePage[_T], Generic[_T]): + blueprints: List[_T] + has_more: Optional[bool] = None + total_count: Optional[int] = None + + @override + def _get_page_items(self) -> List[_T]: + blueprints = self.blueprints + if not blueprints: + return [] + return blueprints + + @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]: + blueprints = self.blueprints + if not blueprints: + return None + + item = cast(Any, blueprints[-1]) + if not isinstance(item, BlueprintsCursorIDPageItem) or item.id is None: # pyright: ignore[reportUnnecessaryComparison] + # TODO emit warning log + return None + + return PageInfo(params={"starting_after": item.id}) + + +class AsyncBlueprintsCursorIDPage(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): + blueprints: List[_T] + has_more: Optional[bool] = None + total_count: Optional[int] = None + + @override + def _get_page_items(self) -> List[_T]: + blueprints = self.blueprints + if not blueprints: + return [] + return blueprints + + @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]: + blueprints = self.blueprints + if not blueprints: + return None + + item = cast(Any, blueprints[-1]) + if not isinstance(item, BlueprintsCursorIDPageItem) or item.id is None: # pyright: ignore[reportUnnecessaryComparison] + # TODO emit warning log + return None + + return PageInfo(params={"starting_after": item.id}) + + +class SyncDevboxesCursorIDPage(BaseSyncPage[_T], BasePage[_T], Generic[_T]): + devboxes: List[_T] + has_more: Optional[bool] = None + total_count: Optional[int] = None + + @override + def _get_page_items(self) -> List[_T]: + devboxes = self.devboxes + if not devboxes: + return [] + return devboxes + + @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]: + devboxes = self.devboxes + if not devboxes: + return None + + item = cast(Any, devboxes[-1]) + if not isinstance(item, DevboxesCursorIDPageItem) or item.id is None: # pyright: ignore[reportUnnecessaryComparison] + # TODO emit warning log + return None + + return PageInfo(params={"starting_after": item.id}) + + +class AsyncDevboxesCursorIDPage(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): + devboxes: List[_T] + has_more: Optional[bool] = None + total_count: Optional[int] = None + + @override + def _get_page_items(self) -> List[_T]: + devboxes = self.devboxes + if not devboxes: + return [] + return devboxes + + @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]: + devboxes = self.devboxes + if not devboxes: + return None + + item = cast(Any, devboxes[-1]) + if not isinstance(item, DevboxesCursorIDPageItem) or item.id is None: # pyright: ignore[reportUnnecessaryComparison] + # TODO emit warning log + return None + + return PageInfo(params={"starting_after": item.id}) + + +class SyncRepositoriesCursorIDPage(BaseSyncPage[_T], BasePage[_T], Generic[_T]): + repositories: List[_T] + has_more: Optional[bool] = None + total_count: Optional[int] = None + + @override + def _get_page_items(self) -> List[_T]: + repositories = self.repositories + if not repositories: + return [] + return repositories + + @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]: + repositories = self.repositories + if not repositories: + return None + + item = cast(Any, repositories[-1]) + if not isinstance(item, RepositoriesCursorIDPageItem) or item.id is None: # pyright: ignore[reportUnnecessaryComparison] + # TODO emit warning log + return None + + return PageInfo(params={"starting_after": item.id}) + + +class AsyncRepositoriesCursorIDPage(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): + repositories: List[_T] + has_more: Optional[bool] = None + total_count: Optional[int] = None + + @override + def _get_page_items(self) -> List[_T]: + repositories = self.repositories + if not repositories: + return [] + return repositories + + @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]: + repositories = self.repositories + if not repositories: + return None + + item = cast(Any, repositories[-1]) + if not isinstance(item, RepositoriesCursorIDPageItem) or item.id is None: # pyright: ignore[reportUnnecessaryComparison] + # TODO emit warning log + return None + + return PageInfo(params={"starting_after": item.id}) + + +class SyncDiskSnapshotsCursorIDPage(BaseSyncPage[_T], BasePage[_T], Generic[_T]): + snapshots: List[_T] + has_more: Optional[bool] = None + total_count: Optional[int] = None + + @override + def _get_page_items(self) -> List[_T]: + snapshots = self.snapshots + if not snapshots: + return [] + return snapshots + + @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]: + snapshots = self.snapshots + if not snapshots: + return None + + item = cast(Any, snapshots[-1]) + if not isinstance(item, DiskSnapshotsCursorIDPageItem) or item.id is None: # pyright: ignore[reportUnnecessaryComparison] + # TODO emit warning log + return None + + return PageInfo(params={"starting_after": item.id}) + + +class AsyncDiskSnapshotsCursorIDPage(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): + snapshots: List[_T] + has_more: Optional[bool] = None + total_count: Optional[int] = None + + @override + def _get_page_items(self) -> List[_T]: + snapshots = self.snapshots + if not snapshots: + return [] + return snapshots + + @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]: + snapshots = self.snapshots + if not snapshots: + return None + + item = cast(Any, snapshots[-1]) + if not isinstance(item, DiskSnapshotsCursorIDPageItem) or item.id is None: # pyright: ignore[reportUnnecessaryComparison] + # TODO emit warning log + return None + + return PageInfo(params={"starting_after": item.id}) + + +class SyncBenchmarksCursorIDPage(BaseSyncPage[_T], BasePage[_T], Generic[_T]): + benchmarks: List[_T] + has_more: Optional[bool] = None + total_count: Optional[int] = None + + @override + def _get_page_items(self) -> List[_T]: + benchmarks = self.benchmarks + if not benchmarks: + return [] + return benchmarks + + @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]: + benchmarks = self.benchmarks + if not benchmarks: + return None + + item = cast(Any, benchmarks[-1]) + if not isinstance(item, BenchmarksCursorIDPageItem) or item.id is None: # pyright: ignore[reportUnnecessaryComparison] + # TODO emit warning log + return None + + return PageInfo(params={"starting_after": item.id}) + + +class AsyncBenchmarksCursorIDPage(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): + benchmarks: List[_T] + has_more: Optional[bool] = None + total_count: Optional[int] = None + + @override + def _get_page_items(self) -> List[_T]: + benchmarks = self.benchmarks + if not benchmarks: + return [] + return benchmarks + + @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]: + benchmarks = self.benchmarks + if not benchmarks: + return None + + item = cast(Any, benchmarks[-1]) + if not isinstance(item, BenchmarksCursorIDPageItem) or item.id is None: # pyright: ignore[reportUnnecessaryComparison] + # TODO emit warning log + return None + + return PageInfo(params={"starting_after": item.id}) + + +class SyncAgentsCursorIDPage(BaseSyncPage[_T], BasePage[_T], Generic[_T]): + agents: List[_T] + has_more: Optional[bool] = None + total_count: Optional[int] = None + + @override + def _get_page_items(self) -> List[_T]: + agents = self.agents + if not agents: + return [] + return agents + + @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]: + agents = self.agents + if not agents: + return None + + item = cast(Any, agents[-1]) + if not isinstance(item, AgentsCursorIDPageItem) or item.id is None: # pyright: ignore[reportUnnecessaryComparison] + # TODO emit warning log + return None + + return PageInfo(params={"starting_after": item.id}) + + +class AsyncAgentsCursorIDPage(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): + agents: List[_T] + has_more: Optional[bool] = None + total_count: Optional[int] = None + + @override + def _get_page_items(self) -> List[_T]: + agents = self.agents + if not agents: + return [] + return agents + + @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]: + agents = self.agents + if not agents: + return None + + item = cast(Any, agents[-1]) + if not isinstance(item, AgentsCursorIDPageItem) or item.id is None: # pyright: ignore[reportUnnecessaryComparison] + # TODO emit warning log + return None + + return PageInfo(params={"starting_after": item.id}) + + +class SyncBenchmarkRunsCursorIDPage(BaseSyncPage[_T], BasePage[_T], Generic[_T]): + runs: List[_T] + has_more: Optional[bool] = None + total_count: Optional[int] = None + + @override + def _get_page_items(self) -> List[_T]: + runs = self.runs + if not runs: + return [] + return runs + + @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]: + runs = self.runs + if not runs: + return None + + item = cast(Any, runs[-1]) + if not isinstance(item, BenchmarkRunsCursorIDPageItem) or item.id is None: # pyright: ignore[reportUnnecessaryComparison] + # TODO emit warning log + return None + + return PageInfo(params={"starting_after": item.id}) + + +class AsyncBenchmarkRunsCursorIDPage(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): + runs: List[_T] + has_more: Optional[bool] = None + total_count: Optional[int] = None + + @override + def _get_page_items(self) -> List[_T]: + runs = self.runs + if not runs: + return [] + return runs + + @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]: + runs = self.runs + if not runs: + return None + + item = cast(Any, runs[-1]) + if not isinstance(item, BenchmarkRunsCursorIDPageItem) or item.id is None: # pyright: ignore[reportUnnecessaryComparison] + # TODO emit warning log + return None + + return PageInfo(params={"starting_after": item.id}) + + +class SyncScenariosCursorIDPage(BaseSyncPage[_T], BasePage[_T], Generic[_T]): + scenarios: List[_T] + has_more: Optional[bool] = None + total_count: Optional[int] = None + + @override + def _get_page_items(self) -> List[_T]: + scenarios = self.scenarios + if not scenarios: + return [] + return scenarios + + @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]: + scenarios = self.scenarios + if not scenarios: + return None + + item = cast(Any, scenarios[-1]) + if not isinstance(item, ScenariosCursorIDPageItem) or item.id is None: # pyright: ignore[reportUnnecessaryComparison] + # TODO emit warning log + return None + + return PageInfo(params={"starting_after": item.id}) + + +class AsyncScenariosCursorIDPage(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): + scenarios: List[_T] + has_more: Optional[bool] = None + total_count: Optional[int] = None + + @override + def _get_page_items(self) -> List[_T]: + scenarios = self.scenarios + if not scenarios: + return [] + return scenarios + + @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]: + scenarios = self.scenarios + if not scenarios: + return None + + item = cast(Any, scenarios[-1]) + if not isinstance(item, ScenariosCursorIDPageItem) or item.id is None: # pyright: ignore[reportUnnecessaryComparison] + # TODO emit warning log + return None + + return PageInfo(params={"starting_after": item.id}) + + +class SyncScenarioRunsCursorIDPage(BaseSyncPage[_T], BasePage[_T], Generic[_T]): + runs: List[_T] + has_more: Optional[bool] = None + total_count: Optional[int] = None + + @override + def _get_page_items(self) -> List[_T]: + runs = self.runs + if not runs: + return [] + return runs + + @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]: + runs = self.runs + if not runs: + return None + + item = cast(Any, runs[-1]) + if not isinstance(item, ScenarioRunsCursorIDPageItem) or item.id is None: # pyright: ignore[reportUnnecessaryComparison] + # TODO emit warning log + return None + + return PageInfo(params={"starting_after": item.id}) + + +class AsyncScenarioRunsCursorIDPage(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): + runs: List[_T] + has_more: Optional[bool] = None + total_count: Optional[int] = None + + @override + def _get_page_items(self) -> List[_T]: + runs = self.runs + if not runs: + return [] + return runs + + @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]: + runs = self.runs + if not runs: + return None + + item = cast(Any, runs[-1]) + if not isinstance(item, ScenarioRunsCursorIDPageItem) or item.id is None: # pyright: ignore[reportUnnecessaryComparison] + # TODO emit warning log + return None + + return PageInfo(params={"starting_after": item.id}) + + +class SyncScenarioScorersCursorIDPage(BaseSyncPage[_T], BasePage[_T], Generic[_T]): + scorers: List[_T] + has_more: Optional[bool] = None + total_count: Optional[int] = None + + @override + def _get_page_items(self) -> List[_T]: + scorers = self.scorers + if not scorers: + return [] + return scorers + + @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]: + scorers = self.scorers + if not scorers: + return None + + item = cast(Any, scorers[-1]) + if not isinstance(item, ScenarioScorersCursorIDPageItem) or item.id is None: # pyright: ignore[reportUnnecessaryComparison] + # TODO emit warning log + return None + + return PageInfo(params={"starting_after": item.id}) + + +class AsyncScenarioScorersCursorIDPage(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): + scorers: List[_T] + has_more: Optional[bool] = None + total_count: Optional[int] = None + + @override + def _get_page_items(self) -> List[_T]: + scorers = self.scorers + if not scorers: + return [] + return scorers + + @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]: + scorers = self.scorers + if not scorers: + return None + + item = cast(Any, scorers[-1]) + if not isinstance(item, ScenarioScorersCursorIDPageItem) or item.id is None: # pyright: ignore[reportUnnecessaryComparison] + # TODO emit warning log + return None + + return PageInfo(params={"starting_after": item.id}) + + +class SyncObjectsCursorIDPage(BaseSyncPage[_T], BasePage[_T], Generic[_T]): + objects: List[_T] + has_more: Optional[bool] = None + total_count: Optional[int] = None + + @override + def _get_page_items(self) -> List[_T]: + objects = self.objects + if not objects: + return [] + return objects + + @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]: + objects = self.objects + if not objects: + return None + + item = cast(Any, objects[-1]) + if not isinstance(item, ObjectsCursorIDPageItem) or item.id is None: # pyright: ignore[reportUnnecessaryComparison] + # TODO emit warning log + return None + + return PageInfo(params={"starting_after": item.id}) + + +class AsyncObjectsCursorIDPage(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): + objects: List[_T] + has_more: Optional[bool] = None + total_count: Optional[int] = None + + @override + def _get_page_items(self) -> List[_T]: + objects = self.objects + if not objects: + return [] + return objects + + @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]: + objects = self.objects + if not objects: + return None + + item = cast(Any, objects[-1]) + if not isinstance(item, ObjectsCursorIDPageItem) or item.id is None: # pyright: ignore[reportUnnecessaryComparison] + # TODO emit warning log + return None + + return PageInfo(params={"starting_after": item.id}) + + +class SyncNetworkPoliciesCursorIDPage(BaseSyncPage[_T], BasePage[_T], Generic[_T]): + network_policies: List[_T] + has_more: Optional[bool] = None + total_count: Optional[int] = None + + @override + def _get_page_items(self) -> List[_T]: + network_policies = self.network_policies + if not network_policies: + return [] + return network_policies + + @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]: + network_policies = self.network_policies + if not network_policies: + return None + + item = cast(Any, network_policies[-1]) + if not isinstance(item, NetworkPoliciesCursorIDPageItem) or item.id is None: # pyright: ignore[reportUnnecessaryComparison] + # TODO emit warning log + return None + + return PageInfo(params={"starting_after": item.id}) + + +class AsyncNetworkPoliciesCursorIDPage(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): + network_policies: List[_T] + has_more: Optional[bool] = None + total_count: Optional[int] = None + + @override + def _get_page_items(self) -> List[_T]: + network_policies = self.network_policies + if not network_policies: + return [] + return network_policies + + @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]: + network_policies = self.network_policies + if not network_policies: + return None + + item = cast(Any, network_policies[-1]) + if not isinstance(item, NetworkPoliciesCursorIDPageItem) or item.id is None: # pyright: ignore[reportUnnecessaryComparison] + # TODO emit warning log + return None + + return PageInfo(params={"starting_after": item.id}) + + +class SyncGatewayConfigsCursorIDPage(BaseSyncPage[_T], BasePage[_T], Generic[_T]): + gateway_configs: List[_T] + has_more: Optional[bool] = None + total_count: Optional[int] = None + + @override + def _get_page_items(self) -> List[_T]: + gateway_configs = self.gateway_configs + if not gateway_configs: + return [] + return gateway_configs + + @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]: + gateway_configs = self.gateway_configs + if not gateway_configs: + return None + + item = cast(Any, gateway_configs[-1]) + if not isinstance(item, GatewayConfigsCursorIDPageItem) or item.id is None: # pyright: ignore[reportUnnecessaryComparison] + # TODO emit warning log + return None + + return PageInfo(params={"starting_after": item.id}) + + +class AsyncGatewayConfigsCursorIDPage(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): + gateway_configs: List[_T] + has_more: Optional[bool] = None + total_count: Optional[int] = None + + @override + def _get_page_items(self) -> List[_T]: + gateway_configs = self.gateway_configs + if not gateway_configs: + return [] + return gateway_configs + + @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]: + gateway_configs = self.gateway_configs + if not gateway_configs: + return None + + item = cast(Any, gateway_configs[-1]) + if not isinstance(item, GatewayConfigsCursorIDPageItem) or item.id is None: # pyright: ignore[reportUnnecessaryComparison] + # TODO emit warning log + return None + + return PageInfo(params={"starting_after": item.id}) + + +class SyncMcpConfigsCursorIDPage(BaseSyncPage[_T], BasePage[_T], Generic[_T]): + mcp_configs: List[_T] + has_more: Optional[bool] = None + total_count: Optional[int] = None + + @override + def _get_page_items(self) -> List[_T]: + mcp_configs = self.mcp_configs + if not mcp_configs: + return [] + return mcp_configs + + @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]: + mcp_configs = self.mcp_configs + if not mcp_configs: + return None + + item = cast(Any, mcp_configs[-1]) + if not isinstance(item, McpConfigsCursorIDPageItem) or item.id is None: # pyright: ignore[reportUnnecessaryComparison] + # TODO emit warning log + return None + + return PageInfo(params={"starting_after": item.id}) + + +class AsyncMcpConfigsCursorIDPage(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): + mcp_configs: List[_T] + has_more: Optional[bool] = None + total_count: Optional[int] = None + + @override + def _get_page_items(self) -> List[_T]: + mcp_configs = self.mcp_configs + if not mcp_configs: + return [] + return mcp_configs + + @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]: + mcp_configs = self.mcp_configs + if not mcp_configs: + return None + + item = cast(Any, mcp_configs[-1]) + if not isinstance(item, McpConfigsCursorIDPageItem) or item.id is None: # pyright: ignore[reportUnnecessaryComparison] + # TODO emit warning log + return None + + return PageInfo(params={"starting_after": item.id}) diff --git a/src/runloop_api_client/py.typed b/src/runloop_api_client/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/src/runloop_api_client/resources/__init__.py b/src/runloop_api_client/resources/__init__.py new file mode 100644 index 000000000..2e0584f25 --- /dev/null +++ b/src/runloop_api_client/resources/__init__.py @@ -0,0 +1,187 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .agents import ( + AgentsResource, + AsyncAgentsResource, + AgentsResourceWithRawResponse, + AsyncAgentsResourceWithRawResponse, + AgentsResourceWithStreamingResponse, + AsyncAgentsResourceWithStreamingResponse, +) +from .objects import ( + ObjectsResource, + AsyncObjectsResource, + ObjectsResourceWithRawResponse, + AsyncObjectsResourceWithRawResponse, + ObjectsResourceWithStreamingResponse, + AsyncObjectsResourceWithStreamingResponse, +) +from .secrets import ( + SecretsResource, + AsyncSecretsResource, + SecretsResourceWithRawResponse, + AsyncSecretsResourceWithRawResponse, + SecretsResourceWithStreamingResponse, + AsyncSecretsResourceWithStreamingResponse, +) +from .devboxes import ( + DevboxesResource, + AsyncDevboxesResource, + DevboxesResourceWithRawResponse, + AsyncDevboxesResourceWithRawResponse, + DevboxesResourceWithStreamingResponse, + AsyncDevboxesResourceWithStreamingResponse, +) +from .scenarios import ( + ScenariosResource, + AsyncScenariosResource, + ScenariosResourceWithRawResponse, + AsyncScenariosResourceWithRawResponse, + ScenariosResourceWithStreamingResponse, + AsyncScenariosResourceWithStreamingResponse, +) +from .benchmarks import ( + BenchmarksResource, + AsyncBenchmarksResource, + BenchmarksResourceWithRawResponse, + AsyncBenchmarksResourceWithRawResponse, + BenchmarksResourceWithStreamingResponse, + AsyncBenchmarksResourceWithStreamingResponse, +) +from .blueprints import ( + BlueprintsResource, + AsyncBlueprintsResource, + BlueprintsResourceWithRawResponse, + AsyncBlueprintsResourceWithRawResponse, + BlueprintsResourceWithStreamingResponse, + AsyncBlueprintsResourceWithStreamingResponse, +) +from .mcp_configs import ( + McpConfigsResource, + AsyncMcpConfigsResource, + McpConfigsResourceWithRawResponse, + AsyncMcpConfigsResourceWithRawResponse, + McpConfigsResourceWithStreamingResponse, + AsyncMcpConfigsResourceWithStreamingResponse, +) +from .repositories import ( + RepositoriesResource, + AsyncRepositoriesResource, + RepositoriesResourceWithRawResponse, + AsyncRepositoriesResourceWithRawResponse, + RepositoriesResourceWithStreamingResponse, + AsyncRepositoriesResourceWithStreamingResponse, +) +from .benchmark_jobs import ( + BenchmarkJobsResource, + AsyncBenchmarkJobsResource, + BenchmarkJobsResourceWithRawResponse, + AsyncBenchmarkJobsResourceWithRawResponse, + BenchmarkJobsResourceWithStreamingResponse, + AsyncBenchmarkJobsResourceWithStreamingResponse, +) +from .benchmark_runs import ( + BenchmarkRunsResource, + AsyncBenchmarkRunsResource, + BenchmarkRunsResourceWithRawResponse, + AsyncBenchmarkRunsResourceWithRawResponse, + BenchmarkRunsResourceWithStreamingResponse, + AsyncBenchmarkRunsResourceWithStreamingResponse, +) +from .gateway_configs import ( + GatewayConfigsResource, + AsyncGatewayConfigsResource, + GatewayConfigsResourceWithRawResponse, + AsyncGatewayConfigsResourceWithRawResponse, + GatewayConfigsResourceWithStreamingResponse, + AsyncGatewayConfigsResourceWithStreamingResponse, +) +from .network_policies import ( + NetworkPoliciesResource, + AsyncNetworkPoliciesResource, + NetworkPoliciesResourceWithRawResponse, + AsyncNetworkPoliciesResourceWithRawResponse, + NetworkPoliciesResourceWithStreamingResponse, + AsyncNetworkPoliciesResourceWithStreamingResponse, +) + +__all__ = [ + "BenchmarksResource", + "AsyncBenchmarksResource", + "BenchmarksResourceWithRawResponse", + "AsyncBenchmarksResourceWithRawResponse", + "BenchmarksResourceWithStreamingResponse", + "AsyncBenchmarksResourceWithStreamingResponse", + "BenchmarkRunsResource", + "AsyncBenchmarkRunsResource", + "BenchmarkRunsResourceWithRawResponse", + "AsyncBenchmarkRunsResourceWithRawResponse", + "BenchmarkRunsResourceWithStreamingResponse", + "AsyncBenchmarkRunsResourceWithStreamingResponse", + "BenchmarkJobsResource", + "AsyncBenchmarkJobsResource", + "BenchmarkJobsResourceWithRawResponse", + "AsyncBenchmarkJobsResourceWithRawResponse", + "BenchmarkJobsResourceWithStreamingResponse", + "AsyncBenchmarkJobsResourceWithStreamingResponse", + "AgentsResource", + "AsyncAgentsResource", + "AgentsResourceWithRawResponse", + "AsyncAgentsResourceWithRawResponse", + "AgentsResourceWithStreamingResponse", + "AsyncAgentsResourceWithStreamingResponse", + "BlueprintsResource", + "AsyncBlueprintsResource", + "BlueprintsResourceWithRawResponse", + "AsyncBlueprintsResourceWithRawResponse", + "BlueprintsResourceWithStreamingResponse", + "AsyncBlueprintsResourceWithStreamingResponse", + "DevboxesResource", + "AsyncDevboxesResource", + "DevboxesResourceWithRawResponse", + "AsyncDevboxesResourceWithRawResponse", + "DevboxesResourceWithStreamingResponse", + "AsyncDevboxesResourceWithStreamingResponse", + "ScenariosResource", + "AsyncScenariosResource", + "ScenariosResourceWithRawResponse", + "AsyncScenariosResourceWithRawResponse", + "ScenariosResourceWithStreamingResponse", + "AsyncScenariosResourceWithStreamingResponse", + "ObjectsResource", + "AsyncObjectsResource", + "ObjectsResourceWithRawResponse", + "AsyncObjectsResourceWithRawResponse", + "ObjectsResourceWithStreamingResponse", + "AsyncObjectsResourceWithStreamingResponse", + "RepositoriesResource", + "AsyncRepositoriesResource", + "RepositoriesResourceWithRawResponse", + "AsyncRepositoriesResourceWithRawResponse", + "RepositoriesResourceWithStreamingResponse", + "AsyncRepositoriesResourceWithStreamingResponse", + "SecretsResource", + "AsyncSecretsResource", + "SecretsResourceWithRawResponse", + "AsyncSecretsResourceWithRawResponse", + "SecretsResourceWithStreamingResponse", + "AsyncSecretsResourceWithStreamingResponse", + "NetworkPoliciesResource", + "AsyncNetworkPoliciesResource", + "NetworkPoliciesResourceWithRawResponse", + "AsyncNetworkPoliciesResourceWithRawResponse", + "NetworkPoliciesResourceWithStreamingResponse", + "AsyncNetworkPoliciesResourceWithStreamingResponse", + "GatewayConfigsResource", + "AsyncGatewayConfigsResource", + "GatewayConfigsResourceWithRawResponse", + "AsyncGatewayConfigsResourceWithRawResponse", + "GatewayConfigsResourceWithStreamingResponse", + "AsyncGatewayConfigsResourceWithStreamingResponse", + "McpConfigsResource", + "AsyncMcpConfigsResource", + "McpConfigsResourceWithRawResponse", + "AsyncMcpConfigsResourceWithRawResponse", + "McpConfigsResourceWithStreamingResponse", + "AsyncMcpConfigsResourceWithStreamingResponse", +] diff --git a/src/runloop_api_client/resources/agents.py b/src/runloop_api_client/resources/agents.py new file mode 100644 index 000000000..9ac9f8c02 --- /dev/null +++ b/src/runloop_api_client/resources/agents.py @@ -0,0 +1,431 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional + +import httpx + +from ..types import agent_list_params, agent_create_params +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..pagination import SyncAgentsCursorIDPage, AsyncAgentsCursorIDPage +from .._base_client import AsyncPaginator, make_request_options +from ..types.agent_view import AgentView +from ..types.shared_params.agent_source import AgentSource + +__all__ = ["AgentsResource", "AsyncAgentsResource"] + + +class AgentsResource(SyncAPIResource): + @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/runloopai/api-client-python#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/runloopai/api-client-python#with_streaming_response + """ + return AgentsResourceWithStreamingResponse(self) + + def create( + self, + *, + name: str, + version: str, + source: Optional[AgentSource] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> AgentView: + """Create a new Agent with a name and optional public visibility. + + The Agent will be + assigned a unique ID. + + Args: + name: The name of the Agent. + + version: The version of the Agent. Must be a semver string (e.g., '2.0.65') or a SHA. + + source: The source configuration for the Agent. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + return self._post( + "/v1/agents", + body=maybe_transform( + { + "name": name, + "version": version, + "source": source, + }, + agent_create_params.AgentCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=AgentView, + ) + + 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, + ) -> AgentView: + """ + Retrieve a specific Agent by its unique identifier. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/v1/agents/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AgentView, + ) + + def list( + self, + *, + is_public: bool | Omit = omit, + limit: int | Omit = omit, + name: str | Omit = omit, + search: str | Omit = omit, + starting_after: 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, + ) -> SyncAgentsCursorIDPage[AgentView]: + """ + List all Agents for the authenticated account with pagination support. + + Args: + is_public: Filter agents by public visibility. + + limit: The limit of items to return. Default is 20. Max is 5000. + + name: Filter agents by name (partial match supported). + + search: Search by agent ID or name. + + starting_after: Load the next page of data starting after the item with the given ID. + + version: Filter by version. Use 'latest' to get the most recently created agent. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters 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( + "/v1/agents", + page=SyncAgentsCursorIDPage[AgentView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "is_public": is_public, + "limit": limit, + "name": name, + "search": search, + "starting_after": starting_after, + "version": version, + }, + agent_list_params.AgentListParams, + ), + ), + model=AgentView, + ) + + +class AsyncAgentsResource(AsyncAPIResource): + @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/runloopai/api-client-python#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/runloopai/api-client-python#with_streaming_response + """ + return AsyncAgentsResourceWithStreamingResponse(self) + + async def create( + self, + *, + name: str, + version: str, + source: Optional[AgentSource] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> AgentView: + """Create a new Agent with a name and optional public visibility. + + The Agent will be + assigned a unique ID. + + Args: + name: The name of the Agent. + + version: The version of the Agent. Must be a semver string (e.g., '2.0.65') or a SHA. + + source: The source configuration for the Agent. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + return await self._post( + "/v1/agents", + body=await async_maybe_transform( + { + "name": name, + "version": version, + "source": source, + }, + agent_create_params.AgentCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=AgentView, + ) + + 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, + ) -> AgentView: + """ + Retrieve a specific Agent by its unique identifier. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/v1/agents/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AgentView, + ) + + def list( + self, + *, + is_public: bool | Omit = omit, + limit: int | Omit = omit, + name: str | Omit = omit, + search: str | Omit = omit, + starting_after: 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, + ) -> AsyncPaginator[AgentView, AsyncAgentsCursorIDPage[AgentView]]: + """ + List all Agents for the authenticated account with pagination support. + + Args: + is_public: Filter agents by public visibility. + + limit: The limit of items to return. Default is 20. Max is 5000. + + name: Filter agents by name (partial match supported). + + search: Search by agent ID or name. + + starting_after: Load the next page of data starting after the item with the given ID. + + version: Filter by version. Use 'latest' to get the most recently created agent. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters 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( + "/v1/agents", + page=AsyncAgentsCursorIDPage[AgentView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "is_public": is_public, + "limit": limit, + "name": name, + "search": search, + "starting_after": starting_after, + "version": version, + }, + agent_list_params.AgentListParams, + ), + ), + model=AgentView, + ) + + +class AgentsResourceWithRawResponse: + def __init__(self, agents: AgentsResource) -> None: + self._agents = agents + + self.create = to_raw_response_wrapper( + agents.create, + ) + self.retrieve = to_raw_response_wrapper( + agents.retrieve, + ) + self.list = to_raw_response_wrapper( + agents.list, + ) + + +class AsyncAgentsResourceWithRawResponse: + def __init__(self, agents: AsyncAgentsResource) -> None: + self._agents = agents + + self.create = async_to_raw_response_wrapper( + agents.create, + ) + self.retrieve = async_to_raw_response_wrapper( + agents.retrieve, + ) + self.list = async_to_raw_response_wrapper( + agents.list, + ) + + +class AgentsResourceWithStreamingResponse: + def __init__(self, agents: AgentsResource) -> None: + self._agents = agents + + self.create = to_streamed_response_wrapper( + agents.create, + ) + self.retrieve = to_streamed_response_wrapper( + agents.retrieve, + ) + self.list = to_streamed_response_wrapper( + agents.list, + ) + + +class AsyncAgentsResourceWithStreamingResponse: + def __init__(self, agents: AsyncAgentsResource) -> None: + self._agents = agents + + self.create = async_to_streamed_response_wrapper( + agents.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + agents.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + agents.list, + ) diff --git a/src/runloop_api_client/resources/benchmark_jobs.py b/src/runloop_api_client/resources/benchmark_jobs.py new file mode 100644 index 000000000..f6172d118 --- /dev/null +++ b/src/runloop_api_client/resources/benchmark_jobs.py @@ -0,0 +1,394 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional + +import httpx + +from ..types import benchmark_job_list_params, benchmark_job_create_params +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.benchmark_job_view import BenchmarkJobView +from ..types.benchmark_job_list_view import BenchmarkJobListView + +__all__ = ["BenchmarkJobsResource", "AsyncBenchmarkJobsResource"] + + +class BenchmarkJobsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> BenchmarkJobsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return BenchmarkJobsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> BenchmarkJobsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return BenchmarkJobsResourceWithStreamingResponse(self) + + def create( + self, + *, + name: Optional[str] | Omit = omit, + spec: Optional[benchmark_job_create_params.Spec] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> BenchmarkJobView: + """ + [Beta] Create a BenchmarkJob that runs a set of scenarios entirely on runloop. + + Args: + name: The name of the BenchmarkJob. If not provided, name will be generated based on + target dataset. + + spec: The job specification. Exactly one spec type must be set. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + return self._post( + "/v1/benchmark_jobs", + body=maybe_transform( + { + "name": name, + "spec": spec, + }, + benchmark_job_create_params.BenchmarkJobCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=BenchmarkJobView, + ) + + 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, + ) -> BenchmarkJobView: + """ + [Beta] Get a BenchmarkJob given 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"/v1/benchmark_jobs/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BenchmarkJobView, + ) + + def list( + self, + *, + limit: int | Omit = omit, + name: str | Omit = omit, + starting_after: 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, + ) -> BenchmarkJobListView: + """ + [Beta] List all BenchmarkJobs matching filter. + + Args: + limit: The limit of items to return. Default is 20. Max is 5000. + + name: Filter by name + + starting_after: Load the next page of data starting after the item with the given ID. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get( + "/v1/benchmark_jobs", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "name": name, + "starting_after": starting_after, + }, + benchmark_job_list_params.BenchmarkJobListParams, + ), + ), + cast_to=BenchmarkJobListView, + ) + + +class AsyncBenchmarkJobsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncBenchmarkJobsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return AsyncBenchmarkJobsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncBenchmarkJobsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return AsyncBenchmarkJobsResourceWithStreamingResponse(self) + + async def create( + self, + *, + name: Optional[str] | Omit = omit, + spec: Optional[benchmark_job_create_params.Spec] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> BenchmarkJobView: + """ + [Beta] Create a BenchmarkJob that runs a set of scenarios entirely on runloop. + + Args: + name: The name of the BenchmarkJob. If not provided, name will be generated based on + target dataset. + + spec: The job specification. Exactly one spec type must be set. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + return await self._post( + "/v1/benchmark_jobs", + body=await async_maybe_transform( + { + "name": name, + "spec": spec, + }, + benchmark_job_create_params.BenchmarkJobCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=BenchmarkJobView, + ) + + 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, + ) -> BenchmarkJobView: + """ + [Beta] Get a BenchmarkJob given 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"/v1/benchmark_jobs/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BenchmarkJobView, + ) + + async def list( + self, + *, + limit: int | Omit = omit, + name: str | Omit = omit, + starting_after: 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, + ) -> BenchmarkJobListView: + """ + [Beta] List all BenchmarkJobs matching filter. + + Args: + limit: The limit of items to return. Default is 20. Max is 5000. + + name: Filter by name + + starting_after: Load the next page of data starting after the item with the given 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._get( + "/v1/benchmark_jobs", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "limit": limit, + "name": name, + "starting_after": starting_after, + }, + benchmark_job_list_params.BenchmarkJobListParams, + ), + ), + cast_to=BenchmarkJobListView, + ) + + +class BenchmarkJobsResourceWithRawResponse: + def __init__(self, benchmark_jobs: BenchmarkJobsResource) -> None: + self._benchmark_jobs = benchmark_jobs + + self.create = to_raw_response_wrapper( + benchmark_jobs.create, + ) + self.retrieve = to_raw_response_wrapper( + benchmark_jobs.retrieve, + ) + self.list = to_raw_response_wrapper( + benchmark_jobs.list, + ) + + +class AsyncBenchmarkJobsResourceWithRawResponse: + def __init__(self, benchmark_jobs: AsyncBenchmarkJobsResource) -> None: + self._benchmark_jobs = benchmark_jobs + + self.create = async_to_raw_response_wrapper( + benchmark_jobs.create, + ) + self.retrieve = async_to_raw_response_wrapper( + benchmark_jobs.retrieve, + ) + self.list = async_to_raw_response_wrapper( + benchmark_jobs.list, + ) + + +class BenchmarkJobsResourceWithStreamingResponse: + def __init__(self, benchmark_jobs: BenchmarkJobsResource) -> None: + self._benchmark_jobs = benchmark_jobs + + self.create = to_streamed_response_wrapper( + benchmark_jobs.create, + ) + self.retrieve = to_streamed_response_wrapper( + benchmark_jobs.retrieve, + ) + self.list = to_streamed_response_wrapper( + benchmark_jobs.list, + ) + + +class AsyncBenchmarkJobsResourceWithStreamingResponse: + def __init__(self, benchmark_jobs: AsyncBenchmarkJobsResource) -> None: + self._benchmark_jobs = benchmark_jobs + + self.create = async_to_streamed_response_wrapper( + benchmark_jobs.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + benchmark_jobs.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + benchmark_jobs.list, + ) diff --git a/src/runloop_api_client/resources/benchmark_runs.py b/src/runloop_api_client/resources/benchmark_runs.py new file mode 100644 index 000000000..3e957de2d --- /dev/null +++ b/src/runloop_api_client/resources/benchmark_runs.py @@ -0,0 +1,595 @@ +# 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 benchmark_run_list_params, benchmark_run_list_scenario_runs_params +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import 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 SyncBenchmarkRunsCursorIDPage, AsyncBenchmarkRunsCursorIDPage +from .._base_client import AsyncPaginator, make_request_options +from ..types.scenario_run_view import ScenarioRunView +from ..types.benchmark_run_view import BenchmarkRunView + +__all__ = ["BenchmarkRunsResource", "AsyncBenchmarkRunsResource"] + + +class BenchmarkRunsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> BenchmarkRunsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return BenchmarkRunsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> BenchmarkRunsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return BenchmarkRunsResourceWithStreamingResponse(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, + ) -> BenchmarkRunView: + """ + Get a BenchmarkRun given 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"/v1/benchmark_runs/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BenchmarkRunView, + ) + + def list( + self, + *, + benchmark_id: str | Omit = omit, + limit: int | Omit = omit, + name: str | Omit = omit, + starting_after: 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, + ) -> SyncBenchmarkRunsCursorIDPage[BenchmarkRunView]: + """ + List all BenchmarkRuns matching filter. + + Args: + benchmark_id: The Benchmark ID to filter by. + + limit: The limit of items to return. Default is 20. Max is 5000. + + name: Filter by name + + starting_after: Load the next page of data starting after the item with the given ID. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/benchmark_runs", + page=SyncBenchmarkRunsCursorIDPage[BenchmarkRunView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "benchmark_id": benchmark_id, + "limit": limit, + "name": name, + "starting_after": starting_after, + }, + benchmark_run_list_params.BenchmarkRunListParams, + ), + ), + model=BenchmarkRunView, + ) + + def cancel( + 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, + idempotency_key: str | None = None, + ) -> BenchmarkRunView: + """ + Cancel a currently running Benchmark run. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/benchmark_runs/{id}/cancel", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=BenchmarkRunView, + ) + + def complete( + 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, + idempotency_key: str | None = None, + ) -> BenchmarkRunView: + """ + Complete a currently running BenchmarkRun. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/benchmark_runs/{id}/complete", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=BenchmarkRunView, + ) + + def list_scenario_runs( + self, + id: str, + *, + limit: int | Omit = omit, + starting_after: str | Omit = omit, + state: Literal["running", "scoring", "scored", "completed", "canceled", "timeout", "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, + ) -> SyncBenchmarkRunsCursorIDPage[ScenarioRunView]: + """ + List started scenario runs for a benchmark run. + + Args: + limit: The limit of items to return. Default is 20. Max is 5000. + + starting_after: Load the next page of data starting after the item with the given ID. + + state: Filter by Scenario Run state + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level 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_api_list( + f"/v1/benchmark_runs/{id}/scenario_runs", + page=SyncBenchmarkRunsCursorIDPage[ScenarioRunView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "starting_after": starting_after, + "state": state, + }, + benchmark_run_list_scenario_runs_params.BenchmarkRunListScenarioRunsParams, + ), + ), + model=ScenarioRunView, + ) + + +class AsyncBenchmarkRunsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncBenchmarkRunsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return AsyncBenchmarkRunsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncBenchmarkRunsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return AsyncBenchmarkRunsResourceWithStreamingResponse(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, + ) -> BenchmarkRunView: + """ + Get a BenchmarkRun given 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"/v1/benchmark_runs/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BenchmarkRunView, + ) + + def list( + self, + *, + benchmark_id: str | Omit = omit, + limit: int | Omit = omit, + name: str | Omit = omit, + starting_after: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[BenchmarkRunView, AsyncBenchmarkRunsCursorIDPage[BenchmarkRunView]]: + """ + List all BenchmarkRuns matching filter. + + Args: + benchmark_id: The Benchmark ID to filter by. + + limit: The limit of items to return. Default is 20. Max is 5000. + + name: Filter by name + + starting_after: Load the next page of data starting after the item with the given ID. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/benchmark_runs", + page=AsyncBenchmarkRunsCursorIDPage[BenchmarkRunView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "benchmark_id": benchmark_id, + "limit": limit, + "name": name, + "starting_after": starting_after, + }, + benchmark_run_list_params.BenchmarkRunListParams, + ), + ), + model=BenchmarkRunView, + ) + + async def cancel( + 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, + idempotency_key: str | None = None, + ) -> BenchmarkRunView: + """ + Cancel a currently running Benchmark run. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/benchmark_runs/{id}/cancel", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=BenchmarkRunView, + ) + + async def complete( + 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, + idempotency_key: str | None = None, + ) -> BenchmarkRunView: + """ + Complete a currently running BenchmarkRun. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/benchmark_runs/{id}/complete", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=BenchmarkRunView, + ) + + def list_scenario_runs( + self, + id: str, + *, + limit: int | Omit = omit, + starting_after: str | Omit = omit, + state: Literal["running", "scoring", "scored", "completed", "canceled", "timeout", "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[ScenarioRunView, AsyncBenchmarkRunsCursorIDPage[ScenarioRunView]]: + """ + List started scenario runs for a benchmark run. + + Args: + limit: The limit of items to return. Default is 20. Max is 5000. + + starting_after: Load the next page of data starting after the item with the given ID. + + state: Filter by Scenario Run state + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level 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_api_list( + f"/v1/benchmark_runs/{id}/scenario_runs", + page=AsyncBenchmarkRunsCursorIDPage[ScenarioRunView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "starting_after": starting_after, + "state": state, + }, + benchmark_run_list_scenario_runs_params.BenchmarkRunListScenarioRunsParams, + ), + ), + model=ScenarioRunView, + ) + + +class BenchmarkRunsResourceWithRawResponse: + def __init__(self, benchmark_runs: BenchmarkRunsResource) -> None: + self._benchmark_runs = benchmark_runs + + self.retrieve = to_raw_response_wrapper( + benchmark_runs.retrieve, + ) + self.list = to_raw_response_wrapper( + benchmark_runs.list, + ) + self.cancel = to_raw_response_wrapper( + benchmark_runs.cancel, + ) + self.complete = to_raw_response_wrapper( + benchmark_runs.complete, + ) + self.list_scenario_runs = to_raw_response_wrapper( + benchmark_runs.list_scenario_runs, + ) + + +class AsyncBenchmarkRunsResourceWithRawResponse: + def __init__(self, benchmark_runs: AsyncBenchmarkRunsResource) -> None: + self._benchmark_runs = benchmark_runs + + self.retrieve = async_to_raw_response_wrapper( + benchmark_runs.retrieve, + ) + self.list = async_to_raw_response_wrapper( + benchmark_runs.list, + ) + self.cancel = async_to_raw_response_wrapper( + benchmark_runs.cancel, + ) + self.complete = async_to_raw_response_wrapper( + benchmark_runs.complete, + ) + self.list_scenario_runs = async_to_raw_response_wrapper( + benchmark_runs.list_scenario_runs, + ) + + +class BenchmarkRunsResourceWithStreamingResponse: + def __init__(self, benchmark_runs: BenchmarkRunsResource) -> None: + self._benchmark_runs = benchmark_runs + + self.retrieve = to_streamed_response_wrapper( + benchmark_runs.retrieve, + ) + self.list = to_streamed_response_wrapper( + benchmark_runs.list, + ) + self.cancel = to_streamed_response_wrapper( + benchmark_runs.cancel, + ) + self.complete = to_streamed_response_wrapper( + benchmark_runs.complete, + ) + self.list_scenario_runs = to_streamed_response_wrapper( + benchmark_runs.list_scenario_runs, + ) + + +class AsyncBenchmarkRunsResourceWithStreamingResponse: + def __init__(self, benchmark_runs: AsyncBenchmarkRunsResource) -> None: + self._benchmark_runs = benchmark_runs + + self.retrieve = async_to_streamed_response_wrapper( + benchmark_runs.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + benchmark_runs.list, + ) + self.cancel = async_to_streamed_response_wrapper( + benchmark_runs.cancel, + ) + self.complete = async_to_streamed_response_wrapper( + benchmark_runs.complete, + ) + self.list_scenario_runs = async_to_streamed_response_wrapper( + benchmark_runs.list_scenario_runs, + ) diff --git a/src/runloop_api_client/resources/benchmarks.py b/src/runloop_api_client/resources/benchmarks.py new file mode 100644 index 000000000..d23992bd2 --- /dev/null +++ b/src/runloop_api_client/resources/benchmarks.py @@ -0,0 +1,1085 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, Optional + +import httpx + +from ..types import ( + benchmark_list_params, + benchmark_create_params, + benchmark_update_params, + benchmark_start_run_params, + benchmark_definitions_params, + benchmark_list_public_params, + benchmark_update_scenarios_params, +) +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 +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..pagination import SyncBenchmarksCursorIDPage, AsyncBenchmarksCursorIDPage +from .._base_client import AsyncPaginator, make_request_options +from ..types.benchmark_view import BenchmarkView +from ..types.benchmark_run_view import BenchmarkRunView +from ..types.shared_params.run_profile import RunProfile +from ..types.scenario_definition_list_view import ScenarioDefinitionListView + +__all__ = ["BenchmarksResource", "AsyncBenchmarksResource"] + + +class BenchmarksResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> BenchmarksResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return BenchmarksResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> BenchmarksResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return BenchmarksResourceWithStreamingResponse(self) + + def create( + self, + *, + name: str, + attribution: Optional[str] | Omit = omit, + description: Optional[str] | Omit = omit, + metadata: Optional[Dict[str, str]] | Omit = omit, + required_environment_variables: Optional[SequenceNotStr[str]] | Omit = omit, + required_secret_names: SequenceNotStr[str] | Omit = omit, + scenario_ids: Optional[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, + idempotency_key: str | None = None, + ) -> BenchmarkView: + """ + Create a Benchmark with a set of Scenarios. + + Args: + name: The unique name of the Benchmark. + + attribution: Attribution information for the benchmark. + + description: Detailed description of the benchmark. + + metadata: User defined metadata to attach to the benchmark. + + required_environment_variables: Environment variables required to run the benchmark. If any required variables + are not supplied, the benchmark will fail to start. + + required_secret_names: Secrets required to run the benchmark with (environment variable name will be + mapped to the your user secret by name). If any of these secrets are not + provided or the mapping is incorrect, the benchmark will fail to start. + + scenario_ids: The Scenario IDs that make up the Benchmark. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + return self._post( + "/v1/benchmarks", + body=maybe_transform( + { + "name": name, + "attribution": attribution, + "description": description, + "metadata": metadata, + "required_environment_variables": required_environment_variables, + "required_secret_names": required_secret_names, + "scenario_ids": scenario_ids, + }, + benchmark_create_params.BenchmarkCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=BenchmarkView, + ) + + 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, + ) -> BenchmarkView: + """ + Get a previously created Benchmark. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/v1/benchmarks/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BenchmarkView, + ) + + def update( + self, + id: str, + *, + attribution: Optional[str] | Omit = omit, + description: Optional[str] | Omit = omit, + metadata: Optional[Dict[str, str]] | Omit = omit, + name: Optional[str] | Omit = omit, + required_environment_variables: Optional[SequenceNotStr[str]] | Omit = omit, + required_secret_names: Optional[SequenceNotStr[str]] | Omit = omit, + scenario_ids: Optional[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, + idempotency_key: str | None = None, + ) -> BenchmarkView: + """Update a Benchmark. + + Fields that are null will preserve the existing value. + Fields that are provided (including empty values) will replace the existing + value entirely. + + Args: + attribution: Attribution information for the benchmark. Pass in empty string to clear. + + description: Detailed description of the benchmark. Pass in empty string to clear. + + metadata: User defined metadata to attach to the benchmark. Pass in empty map to clear. + + name: The unique name of the Benchmark. Cannot be blank. + + required_environment_variables: Environment variables required to run the benchmark. If any required variables + are not supplied, the benchmark will fail to start. Pass in empty list to clear. + + required_secret_names: Secrets required to run the benchmark with (environment variable name will be + mapped to the your user secret by name). If any of these secrets are not + provided or the mapping is incorrect, the benchmark will fail to start. Pass in + empty list to clear. + + scenario_ids: The Scenario IDs that make up the Benchmark. Pass in empty list to clear. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/benchmarks/{id}", + body=maybe_transform( + { + "attribution": attribution, + "description": description, + "metadata": metadata, + "name": name, + "required_environment_variables": required_environment_variables, + "required_secret_names": required_secret_names, + "scenario_ids": scenario_ids, + }, + benchmark_update_params.BenchmarkUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=BenchmarkView, + ) + + def list( + self, + *, + limit: int | Omit = omit, + name: str | Omit = omit, + starting_after: 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, + ) -> SyncBenchmarksCursorIDPage[BenchmarkView]: + """ + List all Benchmarks matching filter. + + Args: + limit: The limit of items to return. Default is 20. Max is 5000. + + name: Filter by name + + starting_after: Load the next page of data starting after the item with the given ID. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/benchmarks", + page=SyncBenchmarksCursorIDPage[BenchmarkView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "name": name, + "starting_after": starting_after, + }, + benchmark_list_params.BenchmarkListParams, + ), + ), + model=BenchmarkView, + ) + + def definitions( + self, + id: str, + *, + limit: int | Omit = omit, + starting_after: 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, + ) -> ScenarioDefinitionListView: + """ + Get scenario definitions for a previously created Benchmark. + + Args: + limit: The limit of items to return. Default is 20. Max is 5000. + + starting_after: Load the next page of data starting after the item with the given 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 + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/v1/benchmarks/{id}/definitions", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "starting_after": starting_after, + }, + benchmark_definitions_params.BenchmarkDefinitionsParams, + ), + ), + cast_to=ScenarioDefinitionListView, + ) + + def list_public( + self, + *, + limit: int | Omit = omit, + starting_after: 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, + ) -> SyncBenchmarksCursorIDPage[BenchmarkView]: + """ + List all public benchmarks matching filter. + + Args: + limit: The limit of items to return. Default is 20. Max is 5000. + + starting_after: Load the next page of data starting after the item with the given ID. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/benchmarks/list_public", + page=SyncBenchmarksCursorIDPage[BenchmarkView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "starting_after": starting_after, + }, + benchmark_list_public_params.BenchmarkListPublicParams, + ), + ), + model=BenchmarkView, + ) + + def start_run( + self, + *, + benchmark_id: str, + metadata: Optional[Dict[str, str]] | Omit = omit, + run_name: Optional[str] | Omit = omit, + run_profile: Optional[RunProfile] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> BenchmarkRunView: + """ + Start a new BenchmarkRun based on the provided Benchmark. + + Args: + benchmark_id: ID of the Benchmark to run. + + metadata: User defined metadata to attach to the benchmark run for organization. + + run_name: Display name of the run. + + run_profile: Runtime configuration to use for this benchmark run + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + return self._post( + "/v1/benchmarks/start_run", + body=maybe_transform( + { + "benchmark_id": benchmark_id, + "metadata": metadata, + "run_name": run_name, + "run_profile": run_profile, + }, + benchmark_start_run_params.BenchmarkStartRunParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=BenchmarkRunView, + ) + + def update_scenarios( + self, + id: str, + *, + scenarios_to_add: Optional[SequenceNotStr[str]] | Omit = omit, + scenarios_to_remove: Optional[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, + idempotency_key: str | None = None, + ) -> BenchmarkView: + """ + Add and/or remove Scenario IDs from an existing Benchmark. + + Args: + scenarios_to_add: Scenario IDs to add to the Benchmark. + + scenarios_to_remove: Scenario IDs to remove from the Benchmark. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/benchmarks/{id}/scenarios", + body=maybe_transform( + { + "scenarios_to_add": scenarios_to_add, + "scenarios_to_remove": scenarios_to_remove, + }, + benchmark_update_scenarios_params.BenchmarkUpdateScenariosParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=BenchmarkView, + ) + + +class AsyncBenchmarksResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncBenchmarksResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return AsyncBenchmarksResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncBenchmarksResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return AsyncBenchmarksResourceWithStreamingResponse(self) + + async def create( + self, + *, + name: str, + attribution: Optional[str] | Omit = omit, + description: Optional[str] | Omit = omit, + metadata: Optional[Dict[str, str]] | Omit = omit, + required_environment_variables: Optional[SequenceNotStr[str]] | Omit = omit, + required_secret_names: SequenceNotStr[str] | Omit = omit, + scenario_ids: Optional[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, + idempotency_key: str | None = None, + ) -> BenchmarkView: + """ + Create a Benchmark with a set of Scenarios. + + Args: + name: The unique name of the Benchmark. + + attribution: Attribution information for the benchmark. + + description: Detailed description of the benchmark. + + metadata: User defined metadata to attach to the benchmark. + + required_environment_variables: Environment variables required to run the benchmark. If any required variables + are not supplied, the benchmark will fail to start. + + required_secret_names: Secrets required to run the benchmark with (environment variable name will be + mapped to the your user secret by name). If any of these secrets are not + provided or the mapping is incorrect, the benchmark will fail to start. + + scenario_ids: The Scenario IDs that make up the Benchmark. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + return await self._post( + "/v1/benchmarks", + body=await async_maybe_transform( + { + "name": name, + "attribution": attribution, + "description": description, + "metadata": metadata, + "required_environment_variables": required_environment_variables, + "required_secret_names": required_secret_names, + "scenario_ids": scenario_ids, + }, + benchmark_create_params.BenchmarkCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=BenchmarkView, + ) + + 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, + ) -> BenchmarkView: + """ + Get a previously created Benchmark. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/v1/benchmarks/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BenchmarkView, + ) + + async def update( + self, + id: str, + *, + attribution: Optional[str] | Omit = omit, + description: Optional[str] | Omit = omit, + metadata: Optional[Dict[str, str]] | Omit = omit, + name: Optional[str] | Omit = omit, + required_environment_variables: Optional[SequenceNotStr[str]] | Omit = omit, + required_secret_names: Optional[SequenceNotStr[str]] | Omit = omit, + scenario_ids: Optional[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, + idempotency_key: str | None = None, + ) -> BenchmarkView: + """Update a Benchmark. + + Fields that are null will preserve the existing value. + Fields that are provided (including empty values) will replace the existing + value entirely. + + Args: + attribution: Attribution information for the benchmark. Pass in empty string to clear. + + description: Detailed description of the benchmark. Pass in empty string to clear. + + metadata: User defined metadata to attach to the benchmark. Pass in empty map to clear. + + name: The unique name of the Benchmark. Cannot be blank. + + required_environment_variables: Environment variables required to run the benchmark. If any required variables + are not supplied, the benchmark will fail to start. Pass in empty list to clear. + + required_secret_names: Secrets required to run the benchmark with (environment variable name will be + mapped to the your user secret by name). If any of these secrets are not + provided or the mapping is incorrect, the benchmark will fail to start. Pass in + empty list to clear. + + scenario_ids: The Scenario IDs that make up the Benchmark. Pass in empty list to clear. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/benchmarks/{id}", + body=await async_maybe_transform( + { + "attribution": attribution, + "description": description, + "metadata": metadata, + "name": name, + "required_environment_variables": required_environment_variables, + "required_secret_names": required_secret_names, + "scenario_ids": scenario_ids, + }, + benchmark_update_params.BenchmarkUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=BenchmarkView, + ) + + def list( + self, + *, + limit: int | Omit = omit, + name: str | Omit = omit, + starting_after: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[BenchmarkView, AsyncBenchmarksCursorIDPage[BenchmarkView]]: + """ + List all Benchmarks matching filter. + + Args: + limit: The limit of items to return. Default is 20. Max is 5000. + + name: Filter by name + + starting_after: Load the next page of data starting after the item with the given ID. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/benchmarks", + page=AsyncBenchmarksCursorIDPage[BenchmarkView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "name": name, + "starting_after": starting_after, + }, + benchmark_list_params.BenchmarkListParams, + ), + ), + model=BenchmarkView, + ) + + async def definitions( + self, + id: str, + *, + limit: int | Omit = omit, + starting_after: 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, + ) -> ScenarioDefinitionListView: + """ + Get scenario definitions for a previously created Benchmark. + + Args: + limit: The limit of items to return. Default is 20. Max is 5000. + + starting_after: Load the next page of data starting after the item with the given 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 + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/v1/benchmarks/{id}/definitions", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "limit": limit, + "starting_after": starting_after, + }, + benchmark_definitions_params.BenchmarkDefinitionsParams, + ), + ), + cast_to=ScenarioDefinitionListView, + ) + + def list_public( + self, + *, + limit: int | Omit = omit, + starting_after: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[BenchmarkView, AsyncBenchmarksCursorIDPage[BenchmarkView]]: + """ + List all public benchmarks matching filter. + + Args: + limit: The limit of items to return. Default is 20. Max is 5000. + + starting_after: Load the next page of data starting after the item with the given ID. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/benchmarks/list_public", + page=AsyncBenchmarksCursorIDPage[BenchmarkView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "starting_after": starting_after, + }, + benchmark_list_public_params.BenchmarkListPublicParams, + ), + ), + model=BenchmarkView, + ) + + async def start_run( + self, + *, + benchmark_id: str, + metadata: Optional[Dict[str, str]] | Omit = omit, + run_name: Optional[str] | Omit = omit, + run_profile: Optional[RunProfile] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> BenchmarkRunView: + """ + Start a new BenchmarkRun based on the provided Benchmark. + + Args: + benchmark_id: ID of the Benchmark to run. + + metadata: User defined metadata to attach to the benchmark run for organization. + + run_name: Display name of the run. + + run_profile: Runtime configuration to use for this benchmark run + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + return await self._post( + "/v1/benchmarks/start_run", + body=await async_maybe_transform( + { + "benchmark_id": benchmark_id, + "metadata": metadata, + "run_name": run_name, + "run_profile": run_profile, + }, + benchmark_start_run_params.BenchmarkStartRunParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=BenchmarkRunView, + ) + + async def update_scenarios( + self, + id: str, + *, + scenarios_to_add: Optional[SequenceNotStr[str]] | Omit = omit, + scenarios_to_remove: Optional[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, + idempotency_key: str | None = None, + ) -> BenchmarkView: + """ + Add and/or remove Scenario IDs from an existing Benchmark. + + Args: + scenarios_to_add: Scenario IDs to add to the Benchmark. + + scenarios_to_remove: Scenario IDs to remove from the Benchmark. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/benchmarks/{id}/scenarios", + body=await async_maybe_transform( + { + "scenarios_to_add": scenarios_to_add, + "scenarios_to_remove": scenarios_to_remove, + }, + benchmark_update_scenarios_params.BenchmarkUpdateScenariosParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=BenchmarkView, + ) + + +class BenchmarksResourceWithRawResponse: + def __init__(self, benchmarks: BenchmarksResource) -> None: + self._benchmarks = benchmarks + + self.create = to_raw_response_wrapper( + benchmarks.create, + ) + self.retrieve = to_raw_response_wrapper( + benchmarks.retrieve, + ) + self.update = to_raw_response_wrapper( + benchmarks.update, + ) + self.list = to_raw_response_wrapper( + benchmarks.list, + ) + self.definitions = to_raw_response_wrapper( + benchmarks.definitions, + ) + self.list_public = to_raw_response_wrapper( + benchmarks.list_public, + ) + self.start_run = to_raw_response_wrapper( + benchmarks.start_run, + ) + self.update_scenarios = to_raw_response_wrapper( + benchmarks.update_scenarios, + ) + + +class AsyncBenchmarksResourceWithRawResponse: + def __init__(self, benchmarks: AsyncBenchmarksResource) -> None: + self._benchmarks = benchmarks + + self.create = async_to_raw_response_wrapper( + benchmarks.create, + ) + self.retrieve = async_to_raw_response_wrapper( + benchmarks.retrieve, + ) + self.update = async_to_raw_response_wrapper( + benchmarks.update, + ) + self.list = async_to_raw_response_wrapper( + benchmarks.list, + ) + self.definitions = async_to_raw_response_wrapper( + benchmarks.definitions, + ) + self.list_public = async_to_raw_response_wrapper( + benchmarks.list_public, + ) + self.start_run = async_to_raw_response_wrapper( + benchmarks.start_run, + ) + self.update_scenarios = async_to_raw_response_wrapper( + benchmarks.update_scenarios, + ) + + +class BenchmarksResourceWithStreamingResponse: + def __init__(self, benchmarks: BenchmarksResource) -> None: + self._benchmarks = benchmarks + + self.create = to_streamed_response_wrapper( + benchmarks.create, + ) + self.retrieve = to_streamed_response_wrapper( + benchmarks.retrieve, + ) + self.update = to_streamed_response_wrapper( + benchmarks.update, + ) + self.list = to_streamed_response_wrapper( + benchmarks.list, + ) + self.definitions = to_streamed_response_wrapper( + benchmarks.definitions, + ) + self.list_public = to_streamed_response_wrapper( + benchmarks.list_public, + ) + self.start_run = to_streamed_response_wrapper( + benchmarks.start_run, + ) + self.update_scenarios = to_streamed_response_wrapper( + benchmarks.update_scenarios, + ) + + +class AsyncBenchmarksResourceWithStreamingResponse: + def __init__(self, benchmarks: AsyncBenchmarksResource) -> None: + self._benchmarks = benchmarks + + self.create = async_to_streamed_response_wrapper( + benchmarks.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + benchmarks.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + benchmarks.update, + ) + self.list = async_to_streamed_response_wrapper( + benchmarks.list, + ) + self.definitions = async_to_streamed_response_wrapper( + benchmarks.definitions, + ) + self.list_public = async_to_streamed_response_wrapper( + benchmarks.list_public, + ) + self.start_run = async_to_streamed_response_wrapper( + benchmarks.start_run, + ) + self.update_scenarios = async_to_streamed_response_wrapper( + benchmarks.update_scenarios, + ) diff --git a/src/runloop_api_client/resources/blueprints.py b/src/runloop_api_client/resources/blueprints.py new file mode 100644 index 000000000..da747d6c4 --- /dev/null +++ b/src/runloop_api_client/resources/blueprints.py @@ -0,0 +1,1251 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, Iterable, Optional + +import httpx + +from ..types import ( + blueprint_list_params, + blueprint_create_params, + blueprint_preview_params, + blueprint_list_public_params, + blueprint_create_from_inspection_params, +) +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 +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..pagination import SyncBlueprintsCursorIDPage, AsyncBlueprintsCursorIDPage +from .._base_client import AsyncPaginator, make_request_options +from ..types.blueprint_view import BlueprintView +from ..types.blueprint_preview_view import BlueprintPreviewView +from ..types.inspection_source_param import InspectionSourceParam +from ..types.blueprint_build_logs_list_view import BlueprintBuildLogsListView +from ..types.shared_params.launch_parameters import LaunchParameters +from ..types.shared_params.code_mount_parameters import CodeMountParameters + +__all__ = ["BlueprintsResource", "AsyncBlueprintsResource"] + + +class BlueprintsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> BlueprintsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return BlueprintsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> BlueprintsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return BlueprintsResourceWithStreamingResponse(self) + + def create( + self, + *, + name: str, + base_blueprint_id: Optional[str] | Omit = omit, + base_blueprint_name: Optional[str] | Omit = omit, + build_args: Optional[Dict[str, str]] | Omit = omit, + build_context: Optional[blueprint_create_params.BuildContext] | Omit = omit, + code_mounts: Optional[Iterable[CodeMountParameters]] | Omit = omit, + dockerfile: Optional[str] | Omit = omit, + file_mounts: Optional[Dict[str, str]] | Omit = omit, + launch_parameters: Optional[LaunchParameters] | Omit = omit, + metadata: Optional[Dict[str, str]] | Omit = omit, + network_policy_id: Optional[str] | Omit = omit, + secrets: Optional[Dict[str, str]] | Omit = omit, + services: Optional[Iterable[blueprint_create_params.Service]] | Omit = omit, + system_setup_commands: Optional[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, + idempotency_key: str | None = None, + ) -> BlueprintView: + """Starts build of custom defined container Blueprint. + + The Blueprint will begin in + the 'provisioning' step and transition to the 'building' step once it is + selected off the build queue., Upon build complete it will transition to + 'building_complete' if the build is successful. + + Args: + name: Name of the Blueprint. + + base_blueprint_id: (Optional) ID of previously built blueprint to use as a base blueprint for this + build. + + base_blueprint_name: (Optional) Name of previously built blueprint to use as a base blueprint for + this build. When set, this will load the latest successfully built Blueprint + with the given name. Only one of (base_blueprint_id, base_blueprint_name) should + be specified. + + build_args: (Optional) Arbitrary Docker build args to pass during build. + + build_context: A build context backed by an Object. + + code_mounts: A list of code mounts to be included in the Blueprint. + + dockerfile: Dockerfile contents to be used to build the Blueprint. + + file_mounts: (Optional) Map of paths and file contents to write before setup. + + launch_parameters: Parameters to configure your Devbox at launch time. + + metadata: (Optional) User defined metadata for the Blueprint. + + network_policy_id: (Optional) ID of the network policy to apply during blueprint build. This + restricts network access during the build process. This does not affect devboxes + created from this blueprint; if you want devboxes created from this blueprint to + inherit the network policy, set the network_policy_id on the blueprint launch + parameters. + + secrets: (Optional) Map of mount IDs/environment variable names to secret names. Secrets + will be available to commands during the build. Secrets are NOT stored in the + blueprint image. Example: {"DB_PASS": "DATABASE_PASSWORD"} makes the secret + 'DATABASE_PASSWORD' available as environment variable 'DB_PASS'. + + services: (Optional) List of containerized services to include in the Blueprint. These + services will be pre-pulled during the build phase for optimized startup + performance. + + system_setup_commands: A list of commands to run to set up your system. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + return self._post( + "/v1/blueprints", + body=maybe_transform( + { + "name": name, + "base_blueprint_id": base_blueprint_id, + "base_blueprint_name": base_blueprint_name, + "build_args": build_args, + "build_context": build_context, + "code_mounts": code_mounts, + "dockerfile": dockerfile, + "file_mounts": file_mounts, + "launch_parameters": launch_parameters, + "metadata": metadata, + "network_policy_id": network_policy_id, + "secrets": secrets, + "services": services, + "system_setup_commands": system_setup_commands, + }, + blueprint_create_params.BlueprintCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=BlueprintView, + ) + + 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, + ) -> BlueprintView: + """ + Get the details of a previously created Blueprint including the build 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"/v1/blueprints/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BlueprintView, + ) + + def list( + self, + *, + limit: int | Omit = omit, + name: str | Omit = omit, + starting_after: str | Omit = omit, + status: 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, + ) -> SyncBlueprintsCursorIDPage[BlueprintView]: + """ + List all Blueprints or filter by name. + + Args: + limit: The limit of items to return. Default is 20. Max is 5000. + + name: Filter by name + + starting_after: Load the next page of data starting after the item with the given ID. + + status: Filter by build status (queued, provisioning, building, failed, build_complete) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters 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( + "/v1/blueprints", + page=SyncBlueprintsCursorIDPage[BlueprintView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "name": name, + "starting_after": starting_after, + "status": status, + }, + blueprint_list_params.BlueprintListParams, + ), + ), + model=BlueprintView, + ) + + 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, + idempotency_key: str | None = None, + ) -> object: + """Delete a previously created Blueprint. + + If a blueprint has dependent snapshots, + it cannot be deleted. You can find them by querying: GET + /v1/devboxes/disk_snapshots?source_blueprint_id={blueprint_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 + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/blueprints/{id}/delete", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=object, + ) + + def create_from_inspection( + self, + *, + inspection_source: InspectionSourceParam, + name: str, + file_mounts: Optional[Dict[str, str]] | Omit = omit, + launch_parameters: Optional[LaunchParameters] | Omit = omit, + metadata: Optional[Dict[str, str]] | Omit = omit, + network_policy_id: Optional[str] | Omit = omit, + secrets: Optional[Dict[str, str]] | Omit = omit, + system_setup_commands: Optional[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, + idempotency_key: str | None = None, + ) -> BlueprintView: + """ + Starts build of custom defined container Blueprint using a RepositoryConnection + Inspection as a source container specification. + + Args: + inspection_source: (Optional) Use a RepositoryInspection a source of a Blueprint build. The + Dockerfile will be automatically created based on the RepositoryInspection + contents. + + name: Name of the Blueprint. + + file_mounts: (Optional) Map of paths and file contents to write before setup. + + launch_parameters: Parameters to configure your Devbox at launch time. + + metadata: (Optional) User defined metadata for the Blueprint. + + network_policy_id: (Optional) ID of the network policy to apply during blueprint build. This + restricts network access during the build process. + + secrets: (Optional) Map of mount IDs/environment variable names to secret names. Secrets + can be used as environment variables in system_setup_commands. Example: + {"GITHUB_TOKEN": "gh_secret"} makes 'gh_secret' available as GITHUB_TOKEN. + + system_setup_commands: A list of commands to run to set up your system. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + return self._post( + "/v1/blueprints/create_from_inspection", + body=maybe_transform( + { + "inspection_source": inspection_source, + "name": name, + "file_mounts": file_mounts, + "launch_parameters": launch_parameters, + "metadata": metadata, + "network_policy_id": network_policy_id, + "secrets": secrets, + "system_setup_commands": system_setup_commands, + }, + blueprint_create_from_inspection_params.BlueprintCreateFromInspectionParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=BlueprintView, + ) + + def list_public( + self, + *, + limit: int | Omit = omit, + name: str | Omit = omit, + starting_after: str | Omit = omit, + status: 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, + ) -> SyncBlueprintsCursorIDPage[BlueprintView]: + """ + List all public Blueprints that are available to all users. + + Args: + limit: The limit of items to return. Default is 20. Max is 5000. + + name: Filter by name + + starting_after: Load the next page of data starting after the item with the given ID. + + status: Filter by build status (queued, provisioning, building, failed, build_complete) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters 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( + "/v1/blueprints/list_public", + page=SyncBlueprintsCursorIDPage[BlueprintView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "name": name, + "starting_after": starting_after, + "status": status, + }, + blueprint_list_public_params.BlueprintListPublicParams, + ), + ), + model=BlueprintView, + ) + + def logs( + 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, + ) -> BlueprintBuildLogsListView: + """ + Get all logs from the building of a Blueprint. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/v1/blueprints/{id}/logs", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BlueprintBuildLogsListView, + ) + + def preview( + self, + *, + name: str, + base_blueprint_id: Optional[str] | Omit = omit, + base_blueprint_name: Optional[str] | Omit = omit, + build_args: Optional[Dict[str, str]] | Omit = omit, + build_context: Optional[blueprint_preview_params.BuildContext] | Omit = omit, + code_mounts: Optional[Iterable[CodeMountParameters]] | Omit = omit, + dockerfile: Optional[str] | Omit = omit, + file_mounts: Optional[Dict[str, str]] | Omit = omit, + launch_parameters: Optional[LaunchParameters] | Omit = omit, + metadata: Optional[Dict[str, str]] | Omit = omit, + network_policy_id: Optional[str] | Omit = omit, + secrets: Optional[Dict[str, str]] | Omit = omit, + services: Optional[Iterable[blueprint_preview_params.Service]] | Omit = omit, + system_setup_commands: Optional[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, + idempotency_key: str | None = None, + ) -> BlueprintPreviewView: + """Preview building a Blueprint with the specified configuration. + + You can take the + resulting Dockerfile and test out your build using any local docker tooling. + + Args: + name: Name of the Blueprint. + + base_blueprint_id: (Optional) ID of previously built blueprint to use as a base blueprint for this + build. + + base_blueprint_name: (Optional) Name of previously built blueprint to use as a base blueprint for + this build. When set, this will load the latest successfully built Blueprint + with the given name. Only one of (base_blueprint_id, base_blueprint_name) should + be specified. + + build_args: (Optional) Arbitrary Docker build args to pass during build. + + build_context: A build context backed by an Object. + + code_mounts: A list of code mounts to be included in the Blueprint. + + dockerfile: Dockerfile contents to be used to build the Blueprint. + + file_mounts: (Optional) Map of paths and file contents to write before setup. + + launch_parameters: Parameters to configure your Devbox at launch time. + + metadata: (Optional) User defined metadata for the Blueprint. + + network_policy_id: (Optional) ID of the network policy to apply during blueprint build. This + restricts network access during the build process. This does not affect devboxes + created from this blueprint; if you want devboxes created from this blueprint to + inherit the network policy, set the network_policy_id on the blueprint launch + parameters. + + secrets: (Optional) Map of mount IDs/environment variable names to secret names. Secrets + will be available to commands during the build. Secrets are NOT stored in the + blueprint image. Example: {"DB_PASS": "DATABASE_PASSWORD"} makes the secret + 'DATABASE_PASSWORD' available as environment variable 'DB_PASS'. + + services: (Optional) List of containerized services to include in the Blueprint. These + services will be pre-pulled during the build phase for optimized startup + performance. + + system_setup_commands: A list of commands to run to set up your system. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + return self._post( + "/v1/blueprints/preview", + body=maybe_transform( + { + "name": name, + "base_blueprint_id": base_blueprint_id, + "base_blueprint_name": base_blueprint_name, + "build_args": build_args, + "build_context": build_context, + "code_mounts": code_mounts, + "dockerfile": dockerfile, + "file_mounts": file_mounts, + "launch_parameters": launch_parameters, + "metadata": metadata, + "network_policy_id": network_policy_id, + "secrets": secrets, + "services": services, + "system_setup_commands": system_setup_commands, + }, + blueprint_preview_params.BlueprintPreviewParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=BlueprintPreviewView, + ) + + +class AsyncBlueprintsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncBlueprintsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return AsyncBlueprintsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncBlueprintsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return AsyncBlueprintsResourceWithStreamingResponse(self) + + async def create( + self, + *, + name: str, + base_blueprint_id: Optional[str] | Omit = omit, + base_blueprint_name: Optional[str] | Omit = omit, + build_args: Optional[Dict[str, str]] | Omit = omit, + build_context: Optional[blueprint_create_params.BuildContext] | Omit = omit, + code_mounts: Optional[Iterable[CodeMountParameters]] | Omit = omit, + dockerfile: Optional[str] | Omit = omit, + file_mounts: Optional[Dict[str, str]] | Omit = omit, + launch_parameters: Optional[LaunchParameters] | Omit = omit, + metadata: Optional[Dict[str, str]] | Omit = omit, + network_policy_id: Optional[str] | Omit = omit, + secrets: Optional[Dict[str, str]] | Omit = omit, + services: Optional[Iterable[blueprint_create_params.Service]] | Omit = omit, + system_setup_commands: Optional[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, + idempotency_key: str | None = None, + ) -> BlueprintView: + """Starts build of custom defined container Blueprint. + + The Blueprint will begin in + the 'provisioning' step and transition to the 'building' step once it is + selected off the build queue., Upon build complete it will transition to + 'building_complete' if the build is successful. + + Args: + name: Name of the Blueprint. + + base_blueprint_id: (Optional) ID of previously built blueprint to use as a base blueprint for this + build. + + base_blueprint_name: (Optional) Name of previously built blueprint to use as a base blueprint for + this build. When set, this will load the latest successfully built Blueprint + with the given name. Only one of (base_blueprint_id, base_blueprint_name) should + be specified. + + build_args: (Optional) Arbitrary Docker build args to pass during build. + + build_context: A build context backed by an Object. + + code_mounts: A list of code mounts to be included in the Blueprint. + + dockerfile: Dockerfile contents to be used to build the Blueprint. + + file_mounts: (Optional) Map of paths and file contents to write before setup. + + launch_parameters: Parameters to configure your Devbox at launch time. + + metadata: (Optional) User defined metadata for the Blueprint. + + network_policy_id: (Optional) ID of the network policy to apply during blueprint build. This + restricts network access during the build process. This does not affect devboxes + created from this blueprint; if you want devboxes created from this blueprint to + inherit the network policy, set the network_policy_id on the blueprint launch + parameters. + + secrets: (Optional) Map of mount IDs/environment variable names to secret names. Secrets + will be available to commands during the build. Secrets are NOT stored in the + blueprint image. Example: {"DB_PASS": "DATABASE_PASSWORD"} makes the secret + 'DATABASE_PASSWORD' available as environment variable 'DB_PASS'. + + services: (Optional) List of containerized services to include in the Blueprint. These + services will be pre-pulled during the build phase for optimized startup + performance. + + system_setup_commands: A list of commands to run to set up your system. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + return await self._post( + "/v1/blueprints", + body=await async_maybe_transform( + { + "name": name, + "base_blueprint_id": base_blueprint_id, + "base_blueprint_name": base_blueprint_name, + "build_args": build_args, + "build_context": build_context, + "code_mounts": code_mounts, + "dockerfile": dockerfile, + "file_mounts": file_mounts, + "launch_parameters": launch_parameters, + "metadata": metadata, + "network_policy_id": network_policy_id, + "secrets": secrets, + "services": services, + "system_setup_commands": system_setup_commands, + }, + blueprint_create_params.BlueprintCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=BlueprintView, + ) + + 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, + ) -> BlueprintView: + """ + Get the details of a previously created Blueprint including the build 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"/v1/blueprints/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BlueprintView, + ) + + def list( + self, + *, + limit: int | Omit = omit, + name: str | Omit = omit, + starting_after: str | Omit = omit, + status: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[BlueprintView, AsyncBlueprintsCursorIDPage[BlueprintView]]: + """ + List all Blueprints or filter by name. + + Args: + limit: The limit of items to return. Default is 20. Max is 5000. + + name: Filter by name + + starting_after: Load the next page of data starting after the item with the given ID. + + status: Filter by build status (queued, provisioning, building, failed, build_complete) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters 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( + "/v1/blueprints", + page=AsyncBlueprintsCursorIDPage[BlueprintView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "name": name, + "starting_after": starting_after, + "status": status, + }, + blueprint_list_params.BlueprintListParams, + ), + ), + model=BlueprintView, + ) + + 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, + idempotency_key: str | None = None, + ) -> object: + """Delete a previously created Blueprint. + + If a blueprint has dependent snapshots, + it cannot be deleted. You can find them by querying: GET + /v1/devboxes/disk_snapshots?source_blueprint_id={blueprint_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 + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/blueprints/{id}/delete", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=object, + ) + + async def create_from_inspection( + self, + *, + inspection_source: InspectionSourceParam, + name: str, + file_mounts: Optional[Dict[str, str]] | Omit = omit, + launch_parameters: Optional[LaunchParameters] | Omit = omit, + metadata: Optional[Dict[str, str]] | Omit = omit, + network_policy_id: Optional[str] | Omit = omit, + secrets: Optional[Dict[str, str]] | Omit = omit, + system_setup_commands: Optional[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, + idempotency_key: str | None = None, + ) -> BlueprintView: + """ + Starts build of custom defined container Blueprint using a RepositoryConnection + Inspection as a source container specification. + + Args: + inspection_source: (Optional) Use a RepositoryInspection a source of a Blueprint build. The + Dockerfile will be automatically created based on the RepositoryInspection + contents. + + name: Name of the Blueprint. + + file_mounts: (Optional) Map of paths and file contents to write before setup. + + launch_parameters: Parameters to configure your Devbox at launch time. + + metadata: (Optional) User defined metadata for the Blueprint. + + network_policy_id: (Optional) ID of the network policy to apply during blueprint build. This + restricts network access during the build process. + + secrets: (Optional) Map of mount IDs/environment variable names to secret names. Secrets + can be used as environment variables in system_setup_commands. Example: + {"GITHUB_TOKEN": "gh_secret"} makes 'gh_secret' available as GITHUB_TOKEN. + + system_setup_commands: A list of commands to run to set up your system. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + return await self._post( + "/v1/blueprints/create_from_inspection", + body=await async_maybe_transform( + { + "inspection_source": inspection_source, + "name": name, + "file_mounts": file_mounts, + "launch_parameters": launch_parameters, + "metadata": metadata, + "network_policy_id": network_policy_id, + "secrets": secrets, + "system_setup_commands": system_setup_commands, + }, + blueprint_create_from_inspection_params.BlueprintCreateFromInspectionParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=BlueprintView, + ) + + def list_public( + self, + *, + limit: int | Omit = omit, + name: str | Omit = omit, + starting_after: str | Omit = omit, + status: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[BlueprintView, AsyncBlueprintsCursorIDPage[BlueprintView]]: + """ + List all public Blueprints that are available to all users. + + Args: + limit: The limit of items to return. Default is 20. Max is 5000. + + name: Filter by name + + starting_after: Load the next page of data starting after the item with the given ID. + + status: Filter by build status (queued, provisioning, building, failed, build_complete) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters 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( + "/v1/blueprints/list_public", + page=AsyncBlueprintsCursorIDPage[BlueprintView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "name": name, + "starting_after": starting_after, + "status": status, + }, + blueprint_list_public_params.BlueprintListPublicParams, + ), + ), + model=BlueprintView, + ) + + async def logs( + 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, + ) -> BlueprintBuildLogsListView: + """ + Get all logs from the building of a Blueprint. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/v1/blueprints/{id}/logs", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BlueprintBuildLogsListView, + ) + + async def preview( + self, + *, + name: str, + base_blueprint_id: Optional[str] | Omit = omit, + base_blueprint_name: Optional[str] | Omit = omit, + build_args: Optional[Dict[str, str]] | Omit = omit, + build_context: Optional[blueprint_preview_params.BuildContext] | Omit = omit, + code_mounts: Optional[Iterable[CodeMountParameters]] | Omit = omit, + dockerfile: Optional[str] | Omit = omit, + file_mounts: Optional[Dict[str, str]] | Omit = omit, + launch_parameters: Optional[LaunchParameters] | Omit = omit, + metadata: Optional[Dict[str, str]] | Omit = omit, + network_policy_id: Optional[str] | Omit = omit, + secrets: Optional[Dict[str, str]] | Omit = omit, + services: Optional[Iterable[blueprint_preview_params.Service]] | Omit = omit, + system_setup_commands: Optional[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, + idempotency_key: str | None = None, + ) -> BlueprintPreviewView: + """Preview building a Blueprint with the specified configuration. + + You can take the + resulting Dockerfile and test out your build using any local docker tooling. + + Args: + name: Name of the Blueprint. + + base_blueprint_id: (Optional) ID of previously built blueprint to use as a base blueprint for this + build. + + base_blueprint_name: (Optional) Name of previously built blueprint to use as a base blueprint for + this build. When set, this will load the latest successfully built Blueprint + with the given name. Only one of (base_blueprint_id, base_blueprint_name) should + be specified. + + build_args: (Optional) Arbitrary Docker build args to pass during build. + + build_context: A build context backed by an Object. + + code_mounts: A list of code mounts to be included in the Blueprint. + + dockerfile: Dockerfile contents to be used to build the Blueprint. + + file_mounts: (Optional) Map of paths and file contents to write before setup. + + launch_parameters: Parameters to configure your Devbox at launch time. + + metadata: (Optional) User defined metadata for the Blueprint. + + network_policy_id: (Optional) ID of the network policy to apply during blueprint build. This + restricts network access during the build process. This does not affect devboxes + created from this blueprint; if you want devboxes created from this blueprint to + inherit the network policy, set the network_policy_id on the blueprint launch + parameters. + + secrets: (Optional) Map of mount IDs/environment variable names to secret names. Secrets + will be available to commands during the build. Secrets are NOT stored in the + blueprint image. Example: {"DB_PASS": "DATABASE_PASSWORD"} makes the secret + 'DATABASE_PASSWORD' available as environment variable 'DB_PASS'. + + services: (Optional) List of containerized services to include in the Blueprint. These + services will be pre-pulled during the build phase for optimized startup + performance. + + system_setup_commands: A list of commands to run to set up your system. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + return await self._post( + "/v1/blueprints/preview", + body=await async_maybe_transform( + { + "name": name, + "base_blueprint_id": base_blueprint_id, + "base_blueprint_name": base_blueprint_name, + "build_args": build_args, + "build_context": build_context, + "code_mounts": code_mounts, + "dockerfile": dockerfile, + "file_mounts": file_mounts, + "launch_parameters": launch_parameters, + "metadata": metadata, + "network_policy_id": network_policy_id, + "secrets": secrets, + "services": services, + "system_setup_commands": system_setup_commands, + }, + blueprint_preview_params.BlueprintPreviewParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=BlueprintPreviewView, + ) + + +class BlueprintsResourceWithRawResponse: + def __init__(self, blueprints: BlueprintsResource) -> None: + self._blueprints = blueprints + + self.create = to_raw_response_wrapper( + blueprints.create, + ) + self.retrieve = to_raw_response_wrapper( + blueprints.retrieve, + ) + self.list = to_raw_response_wrapper( + blueprints.list, + ) + self.delete = to_raw_response_wrapper( + blueprints.delete, + ) + self.create_from_inspection = to_raw_response_wrapper( + blueprints.create_from_inspection, + ) + self.list_public = to_raw_response_wrapper( + blueprints.list_public, + ) + self.logs = to_raw_response_wrapper( + blueprints.logs, + ) + self.preview = to_raw_response_wrapper( + blueprints.preview, + ) + + +class AsyncBlueprintsResourceWithRawResponse: + def __init__(self, blueprints: AsyncBlueprintsResource) -> None: + self._blueprints = blueprints + + self.create = async_to_raw_response_wrapper( + blueprints.create, + ) + self.retrieve = async_to_raw_response_wrapper( + blueprints.retrieve, + ) + self.list = async_to_raw_response_wrapper( + blueprints.list, + ) + self.delete = async_to_raw_response_wrapper( + blueprints.delete, + ) + self.create_from_inspection = async_to_raw_response_wrapper( + blueprints.create_from_inspection, + ) + self.list_public = async_to_raw_response_wrapper( + blueprints.list_public, + ) + self.logs = async_to_raw_response_wrapper( + blueprints.logs, + ) + self.preview = async_to_raw_response_wrapper( + blueprints.preview, + ) + + +class BlueprintsResourceWithStreamingResponse: + def __init__(self, blueprints: BlueprintsResource) -> None: + self._blueprints = blueprints + + self.create = to_streamed_response_wrapper( + blueprints.create, + ) + self.retrieve = to_streamed_response_wrapper( + blueprints.retrieve, + ) + self.list = to_streamed_response_wrapper( + blueprints.list, + ) + self.delete = to_streamed_response_wrapper( + blueprints.delete, + ) + self.create_from_inspection = to_streamed_response_wrapper( + blueprints.create_from_inspection, + ) + self.list_public = to_streamed_response_wrapper( + blueprints.list_public, + ) + self.logs = to_streamed_response_wrapper( + blueprints.logs, + ) + self.preview = to_streamed_response_wrapper( + blueprints.preview, + ) + + +class AsyncBlueprintsResourceWithStreamingResponse: + def __init__(self, blueprints: AsyncBlueprintsResource) -> None: + self._blueprints = blueprints + + self.create = async_to_streamed_response_wrapper( + blueprints.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + blueprints.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + blueprints.list, + ) + self.delete = async_to_streamed_response_wrapper( + blueprints.delete, + ) + self.create_from_inspection = async_to_streamed_response_wrapper( + blueprints.create_from_inspection, + ) + self.list_public = async_to_streamed_response_wrapper( + blueprints.list_public, + ) + self.logs = async_to_streamed_response_wrapper( + blueprints.logs, + ) + self.preview = async_to_streamed_response_wrapper( + blueprints.preview, + ) diff --git a/src/runloop_api_client/resources/devboxes/__init__.py b/src/runloop_api_client/resources/devboxes/__init__.py new file mode 100644 index 000000000..164104c7b --- /dev/null +++ b/src/runloop_api_client/resources/devboxes/__init__.py @@ -0,0 +1,89 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .logs import ( + LogsResource, + AsyncLogsResource, + LogsResourceWithRawResponse, + AsyncLogsResourceWithRawResponse, + LogsResourceWithStreamingResponse, + AsyncLogsResourceWithStreamingResponse, +) +from .browsers import ( + BrowsersResource, + AsyncBrowsersResource, + BrowsersResourceWithRawResponse, + AsyncBrowsersResourceWithRawResponse, + BrowsersResourceWithStreamingResponse, + AsyncBrowsersResourceWithStreamingResponse, +) +from .devboxes import ( + DevboxesResource, + AsyncDevboxesResource, + DevboxesResourceWithRawResponse, + AsyncDevboxesResourceWithRawResponse, + DevboxesResourceWithStreamingResponse, + AsyncDevboxesResourceWithStreamingResponse, +) +from .computers import ( + ComputersResource, + AsyncComputersResource, + ComputersResourceWithRawResponse, + AsyncComputersResourceWithRawResponse, + ComputersResourceWithStreamingResponse, + AsyncComputersResourceWithStreamingResponse, +) +from .executions import ( + ExecutionsResource, + AsyncExecutionsResource, + ExecutionsResourceWithRawResponse, + AsyncExecutionsResourceWithRawResponse, + ExecutionsResourceWithStreamingResponse, + AsyncExecutionsResourceWithStreamingResponse, +) +from .disk_snapshots import ( + DiskSnapshotsResource, + AsyncDiskSnapshotsResource, + DiskSnapshotsResourceWithRawResponse, + AsyncDiskSnapshotsResourceWithRawResponse, + DiskSnapshotsResourceWithStreamingResponse, + AsyncDiskSnapshotsResourceWithStreamingResponse, +) + +__all__ = [ + "DiskSnapshotsResource", + "AsyncDiskSnapshotsResource", + "DiskSnapshotsResourceWithRawResponse", + "AsyncDiskSnapshotsResourceWithRawResponse", + "DiskSnapshotsResourceWithStreamingResponse", + "AsyncDiskSnapshotsResourceWithStreamingResponse", + "BrowsersResource", + "AsyncBrowsersResource", + "BrowsersResourceWithRawResponse", + "AsyncBrowsersResourceWithRawResponse", + "BrowsersResourceWithStreamingResponse", + "AsyncBrowsersResourceWithStreamingResponse", + "ComputersResource", + "AsyncComputersResource", + "ComputersResourceWithRawResponse", + "AsyncComputersResourceWithRawResponse", + "ComputersResourceWithStreamingResponse", + "AsyncComputersResourceWithStreamingResponse", + "LogsResource", + "AsyncLogsResource", + "LogsResourceWithRawResponse", + "AsyncLogsResourceWithRawResponse", + "LogsResourceWithStreamingResponse", + "AsyncLogsResourceWithStreamingResponse", + "ExecutionsResource", + "AsyncExecutionsResource", + "ExecutionsResourceWithRawResponse", + "AsyncExecutionsResourceWithRawResponse", + "ExecutionsResourceWithStreamingResponse", + "AsyncExecutionsResourceWithStreamingResponse", + "DevboxesResource", + "AsyncDevboxesResource", + "DevboxesResourceWithRawResponse", + "AsyncDevboxesResourceWithRawResponse", + "DevboxesResourceWithStreamingResponse", + "AsyncDevboxesResourceWithStreamingResponse", +] diff --git a/src/runloop_api_client/resources/devboxes/browsers.py b/src/runloop_api_client/resources/devboxes/browsers.py new file mode 100644 index 000000000..517857f56 --- /dev/null +++ b/src/runloop_api_client/resources/devboxes/browsers.py @@ -0,0 +1,267 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional + +import httpx + +from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from ..._utils import maybe_transform, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..._base_client import make_request_options +from ...types.devboxes import browser_create_params +from ...types.devboxes.browser_view import BrowserView + +__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/runloopai/api-client-python#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/runloopai/api-client-python#with_streaming_response + """ + return BrowsersResourceWithStreamingResponse(self) + + def create( + self, + *, + name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> BrowserView: + """Create a Devbox that has a managed Browser and begin the boot process. + + As part + of booting the Devbox, the browser will automatically be started with connection + utilities activated. + + Args: + name: The name to use for the created Devbox with a 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 + + idempotency_key: Specify a custom idempotency key for this request + """ + return self._post( + "/v1/devboxes/browsers", + body=maybe_transform({"name": name}, browser_create_params.BrowserCreateParams), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=BrowserView, + ) + + 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, + ) -> BrowserView: + """ + Get Browser Details. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/v1/devboxes/browsers/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BrowserView, + ) + + +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/runloopai/api-client-python#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/runloopai/api-client-python#with_streaming_response + """ + return AsyncBrowsersResourceWithStreamingResponse(self) + + async def create( + self, + *, + name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> BrowserView: + """Create a Devbox that has a managed Browser and begin the boot process. + + As part + of booting the Devbox, the browser will automatically be started with connection + utilities activated. + + Args: + name: The name to use for the created Devbox with a 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 + + idempotency_key: Specify a custom idempotency key for this request + """ + return await self._post( + "/v1/devboxes/browsers", + body=await async_maybe_transform({"name": name}, browser_create_params.BrowserCreateParams), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=BrowserView, + ) + + 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, + ) -> BrowserView: + """ + Get Browser Details. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/v1/devboxes/browsers/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BrowserView, + ) + + +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/runloop_api_client/resources/devboxes/computers.py b/src/runloop_api_client/resources/devboxes/computers.py new file mode 100644 index 000000000..be051db2e --- /dev/null +++ b/src/runloop_api_client/resources/devboxes/computers.py @@ -0,0 +1,648 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Literal + +import httpx + +from ..._types import 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.devboxes import ( + computer_create_params, + computer_mouse_interaction_params, + computer_screen_interaction_params, + computer_keyboard_interaction_params, +) +from ...types.devboxes.computer_view import ComputerView +from ...types.devboxes.computer_mouse_interaction_response import ComputerMouseInteractionResponse +from ...types.devboxes.computer_screen_interaction_response import ComputerScreenInteractionResponse +from ...types.devboxes.computer_keyboard_interaction_response import ComputerKeyboardInteractionResponse + +__all__ = ["ComputersResource", "AsyncComputersResource"] + + +class ComputersResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> ComputersResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return ComputersResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ComputersResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return ComputersResourceWithStreamingResponse(self) + + def create( + self, + *, + display_dimensions: Optional[computer_create_params.DisplayDimensions] | Omit = omit, + name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> ComputerView: + """Create a Computer and begin the boot process. + + The Computer will initially launch + in the 'provisioning' state while Runloop allocates the necessary + infrastructure. It will transition to the 'initializing' state while the booted + Computer runs any Runloop or user defined set up scripts. Finally, the Computer + will transition to the 'running' state when it is ready for use. + + Args: + display_dimensions: Customize the dimensions of the computer display. + + name: The name to use for the created computer. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + return self._post( + "/v1/devboxes/computers", + body=maybe_transform( + { + "display_dimensions": display_dimensions, + "name": name, + }, + computer_create_params.ComputerCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ComputerView, + ) + + 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, + ) -> ComputerView: + """ + Get Computer Details. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/v1/devboxes/computers/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ComputerView, + ) + + def keyboard_interaction( + self, + id: str, + *, + action: Literal["key", "type"], + text: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> ComputerKeyboardInteractionResponse: + """ + Perform the specified keyboard interaction on the Computer identified by the + given ID. + + Args: + action: The keyboard action to perform. + + text: The text to type or the key (with optional modifier) to press. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/devboxes/computers/{id}/keyboard_interaction", + body=maybe_transform( + { + "action": action, + "text": text, + }, + computer_keyboard_interaction_params.ComputerKeyboardInteractionParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ComputerKeyboardInteractionResponse, + ) + + def mouse_interaction( + self, + id: str, + *, + action: Literal["mouse_move", "left_click", "left_click_drag", "right_click", "middle_click", "double_click"], + coordinate: Optional[computer_mouse_interaction_params.Coordinate] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> ComputerMouseInteractionResponse: + """ + Perform the specified mouse interaction on the Computer identified by the given + ID. + + Args: + action: The mouse action to perform. + + coordinate: The x (pixels from the left) and y (pixels from the top) coordinates for the + mouse to move or click-drag. Required only by `action=mouse_move` or + `action=left_click_drag` + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/devboxes/computers/{id}/mouse_interaction", + body=maybe_transform( + { + "action": action, + "coordinate": coordinate, + }, + computer_mouse_interaction_params.ComputerMouseInteractionParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ComputerMouseInteractionResponse, + ) + + def screen_interaction( + self, + id: str, + *, + action: Literal["screenshot", "cursor_position"], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> ComputerScreenInteractionResponse: + """ + Perform the specified screen interaction on the Computer identified by the given + ID. + + Args: + action: The screen action to perform. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/devboxes/computers/{id}/screen_interaction", + body=maybe_transform( + {"action": action}, computer_screen_interaction_params.ComputerScreenInteractionParams + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ComputerScreenInteractionResponse, + ) + + +class AsyncComputersResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncComputersResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return AsyncComputersResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncComputersResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return AsyncComputersResourceWithStreamingResponse(self) + + async def create( + self, + *, + display_dimensions: Optional[computer_create_params.DisplayDimensions] | Omit = omit, + name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> ComputerView: + """Create a Computer and begin the boot process. + + The Computer will initially launch + in the 'provisioning' state while Runloop allocates the necessary + infrastructure. It will transition to the 'initializing' state while the booted + Computer runs any Runloop or user defined set up scripts. Finally, the Computer + will transition to the 'running' state when it is ready for use. + + Args: + display_dimensions: Customize the dimensions of the computer display. + + name: The name to use for the created computer. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + return await self._post( + "/v1/devboxes/computers", + body=await async_maybe_transform( + { + "display_dimensions": display_dimensions, + "name": name, + }, + computer_create_params.ComputerCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ComputerView, + ) + + 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, + ) -> ComputerView: + """ + Get Computer Details. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/v1/devboxes/computers/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ComputerView, + ) + + async def keyboard_interaction( + self, + id: str, + *, + action: Literal["key", "type"], + text: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> ComputerKeyboardInteractionResponse: + """ + Perform the specified keyboard interaction on the Computer identified by the + given ID. + + Args: + action: The keyboard action to perform. + + text: The text to type or the key (with optional modifier) to press. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/devboxes/computers/{id}/keyboard_interaction", + body=await async_maybe_transform( + { + "action": action, + "text": text, + }, + computer_keyboard_interaction_params.ComputerKeyboardInteractionParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ComputerKeyboardInteractionResponse, + ) + + async def mouse_interaction( + self, + id: str, + *, + action: Literal["mouse_move", "left_click", "left_click_drag", "right_click", "middle_click", "double_click"], + coordinate: Optional[computer_mouse_interaction_params.Coordinate] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> ComputerMouseInteractionResponse: + """ + Perform the specified mouse interaction on the Computer identified by the given + ID. + + Args: + action: The mouse action to perform. + + coordinate: The x (pixels from the left) and y (pixels from the top) coordinates for the + mouse to move or click-drag. Required only by `action=mouse_move` or + `action=left_click_drag` + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/devboxes/computers/{id}/mouse_interaction", + body=await async_maybe_transform( + { + "action": action, + "coordinate": coordinate, + }, + computer_mouse_interaction_params.ComputerMouseInteractionParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ComputerMouseInteractionResponse, + ) + + async def screen_interaction( + self, + id: str, + *, + action: Literal["screenshot", "cursor_position"], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> ComputerScreenInteractionResponse: + """ + Perform the specified screen interaction on the Computer identified by the given + ID. + + Args: + action: The screen action to perform. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/devboxes/computers/{id}/screen_interaction", + body=await async_maybe_transform( + {"action": action}, computer_screen_interaction_params.ComputerScreenInteractionParams + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ComputerScreenInteractionResponse, + ) + + +class ComputersResourceWithRawResponse: + def __init__(self, computers: ComputersResource) -> None: + self._computers = computers + + self.create = to_raw_response_wrapper( + computers.create, + ) + self.retrieve = to_raw_response_wrapper( + computers.retrieve, + ) + self.keyboard_interaction = to_raw_response_wrapper( + computers.keyboard_interaction, + ) + self.mouse_interaction = to_raw_response_wrapper( + computers.mouse_interaction, + ) + self.screen_interaction = to_raw_response_wrapper( + computers.screen_interaction, + ) + + +class AsyncComputersResourceWithRawResponse: + def __init__(self, computers: AsyncComputersResource) -> None: + self._computers = computers + + self.create = async_to_raw_response_wrapper( + computers.create, + ) + self.retrieve = async_to_raw_response_wrapper( + computers.retrieve, + ) + self.keyboard_interaction = async_to_raw_response_wrapper( + computers.keyboard_interaction, + ) + self.mouse_interaction = async_to_raw_response_wrapper( + computers.mouse_interaction, + ) + self.screen_interaction = async_to_raw_response_wrapper( + computers.screen_interaction, + ) + + +class ComputersResourceWithStreamingResponse: + def __init__(self, computers: ComputersResource) -> None: + self._computers = computers + + self.create = to_streamed_response_wrapper( + computers.create, + ) + self.retrieve = to_streamed_response_wrapper( + computers.retrieve, + ) + self.keyboard_interaction = to_streamed_response_wrapper( + computers.keyboard_interaction, + ) + self.mouse_interaction = to_streamed_response_wrapper( + computers.mouse_interaction, + ) + self.screen_interaction = to_streamed_response_wrapper( + computers.screen_interaction, + ) + + +class AsyncComputersResourceWithStreamingResponse: + def __init__(self, computers: AsyncComputersResource) -> None: + self._computers = computers + + self.create = async_to_streamed_response_wrapper( + computers.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + computers.retrieve, + ) + self.keyboard_interaction = async_to_streamed_response_wrapper( + computers.keyboard_interaction, + ) + self.mouse_interaction = async_to_streamed_response_wrapper( + computers.mouse_interaction, + ) + self.screen_interaction = async_to_streamed_response_wrapper( + computers.screen_interaction, + ) diff --git a/src/runloop_api_client/resources/devboxes/devboxes.py b/src/runloop_api_client/resources/devboxes/devboxes.py new file mode 100644 index 000000000..37c67279e --- /dev/null +++ b/src/runloop_api_client/resources/devboxes/devboxes.py @@ -0,0 +1,3421 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import typing_extensions +from typing import Dict, List, Mapping, Iterable, Optional, cast +from typing_extensions import Literal + +import httpx + +from .logs import ( + LogsResource, + AsyncLogsResource, + LogsResourceWithRawResponse, + AsyncLogsResourceWithRawResponse, + LogsResourceWithStreamingResponse, + AsyncLogsResourceWithStreamingResponse, +) +from ...types import ( + devbox_list_params, + devbox_create_params, + devbox_update_params, + devbox_execute_params, + devbox_shutdown_params, + devbox_upload_file_params, + devbox_execute_sync_params, + devbox_create_tunnel_params, + devbox_download_file_params, + devbox_enable_tunnel_params, + devbox_execute_async_params, + devbox_remove_tunnel_params, + devbox_snapshot_disk_params, + devbox_wait_for_command_params, + devbox_read_file_contents_params, + devbox_list_disk_snapshots_params, + devbox_snapshot_disk_async_params, + devbox_write_file_contents_params, +) +from ..._types import Body, Omit, Query, Headers, NotGiven, FileTypes, omit, not_given +from ..._utils import is_given, extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform +from .browsers import ( + BrowsersResource, + AsyncBrowsersResource, + BrowsersResourceWithRawResponse, + AsyncBrowsersResourceWithRawResponse, + BrowsersResourceWithStreamingResponse, + AsyncBrowsersResourceWithStreamingResponse, +) +from ..._compat import cached_property +from .computers import ( + ComputersResource, + AsyncComputersResource, + ComputersResourceWithRawResponse, + AsyncComputersResourceWithRawResponse, + ComputersResourceWithStreamingResponse, + AsyncComputersResourceWithStreamingResponse, +) +from .executions import ( + ExecutionsResource, + AsyncExecutionsResource, + ExecutionsResourceWithRawResponse, + AsyncExecutionsResourceWithRawResponse, + ExecutionsResourceWithStreamingResponse, + AsyncExecutionsResourceWithStreamingResponse, +) +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 ..._constants import DEFAULT_TIMEOUT +from ...pagination import ( + SyncDevboxesCursorIDPage, + AsyncDevboxesCursorIDPage, + SyncDiskSnapshotsCursorIDPage, + AsyncDiskSnapshotsCursorIDPage, +) +from ..._base_client import AsyncPaginator, make_request_options +from .disk_snapshots import ( + DiskSnapshotsResource, + AsyncDiskSnapshotsResource, + DiskSnapshotsResourceWithRawResponse, + AsyncDiskSnapshotsResourceWithRawResponse, + DiskSnapshotsResourceWithStreamingResponse, + AsyncDiskSnapshotsResourceWithStreamingResponse, +) +from ...types.devbox_view import DevboxView +from ...types.tunnel_view import TunnelView +from ...types.devbox_tunnel_view import DevboxTunnelView +from ...types.shared_params.mount import Mount +from ...types.devbox_snapshot_view import DevboxSnapshotView +from ...types.devbox_resource_usage_view import DevboxResourceUsageView +from ...types.devbox_execution_detail_view import DevboxExecutionDetailView +from ...types.devbox_create_ssh_key_response import DevboxCreateSSHKeyResponse +from ...types.shared_params.launch_parameters import LaunchParameters +from ...types.devbox_async_execution_detail_view import DevboxAsyncExecutionDetailView +from ...types.shared_params.code_mount_parameters import CodeMountParameters + +__all__ = ["DevboxesResource", "AsyncDevboxesResource"] + + +class DevboxesResource(SyncAPIResource): + @cached_property + def disk_snapshots(self) -> DiskSnapshotsResource: + return DiskSnapshotsResource(self._client) + + @cached_property + def browsers(self) -> BrowsersResource: + return BrowsersResource(self._client) + + @cached_property + def computers(self) -> ComputersResource: + return ComputersResource(self._client) + + @cached_property + def logs(self) -> LogsResource: + return LogsResource(self._client) + + @cached_property + def executions(self) -> ExecutionsResource: + return ExecutionsResource(self._client) + + @cached_property + def with_raw_response(self) -> DevboxesResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return DevboxesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> DevboxesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return DevboxesResourceWithStreamingResponse(self) + + def create( + self, + *, + blueprint_id: Optional[str] | Omit = omit, + blueprint_name: Optional[str] | Omit = omit, + code_mounts: Optional[Iterable[CodeMountParameters]] | Omit = omit, + entrypoint: Optional[str] | Omit = omit, + environment_variables: Optional[Dict[str, str]] | Omit = omit, + file_mounts: Optional[Dict[str, str]] | Omit = omit, + gateways: Optional[Dict[str, devbox_create_params.Gateways]] | Omit = omit, + launch_parameters: Optional[LaunchParameters] | Omit = omit, + mcp: Optional[Dict[str, devbox_create_params.Mcp]] | Omit = omit, + metadata: Optional[Dict[str, str]] | Omit = omit, + mounts: Optional[Iterable[Mount]] | Omit = omit, + name: Optional[str] | Omit = omit, + repo_connection_id: Optional[str] | Omit = omit, + secrets: Optional[Dict[str, str]] | Omit = omit, + snapshot_id: Optional[str] | Omit = omit, + tunnel: Optional[devbox_create_params.Tunnel] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> DevboxView: + """Create a Devbox and begin the boot process. + + The Devbox will initially launch in + the 'provisioning' state while Runloop allocates the necessary infrastructure. + It will transition to the 'initializing' state while the booted Devbox runs any + Runloop or user defined set up scripts. Finally, the Devbox will transition to + the 'running' state when it is ready for use. + + Args: + blueprint_id: Blueprint ID to use for the Devbox. If none set, the Devbox will be created with + the default Runloop Devbox image. Only one of (Snapshot ID, Blueprint ID, + Blueprint name) should be specified. + + blueprint_name: Name of Blueprint to use for the Devbox. When set, this will load the latest + successfully built Blueprint with the given name. Only one of (Snapshot ID, + Blueprint ID, Blueprint name) should be specified. + + code_mounts: A list of code mounts to be included in the Devbox. Use mounts instead. + + entrypoint: (Optional) When specified, the Devbox will run this script as its main + executable. The devbox lifecycle will be bound to entrypoint, shutting down when + the process is complete. + + environment_variables: (Optional) Environment variables used to configure your Devbox. + + file_mounts: Map of paths and file contents to write before setup. Use mounts instead. + + gateways: (Optional) Agent gateway specifications for credential proxying. Map key is the + environment variable prefix (e.g., 'GWS_ANTHROPIC'). The agent gateway will + proxy requests to external APIs using the specified credential without exposing + the real API key. Example: {'GWS_ANTHROPIC': {'gateway': 'anthropic', 'secret': + 'my_claude_key'}} + + launch_parameters: Parameters to configure the resources and launch time behavior of the Devbox. + + mcp: [Beta] (Optional) MCP specifications for MCP server access. Map key is the + environment variable name for the MCP token envelope. Each spec links an MCP + config to a secret. The devbox will also receive RL_MCP_URL for the MCP hub + endpoint. Example: {'MCP_SECRET': {'mcp_config': 'github-readonly', 'secret': + 'MY_GITHUB_TOKEN'}} + + metadata: User defined metadata to attach to the devbox for organization. + + mounts: A list of mounts to be included in the Devbox. + + name: (Optional) A user specified name to give the Devbox. + + repo_connection_id: Repository connection id the devbox should source its base image from. + + secrets: (Optional) Map of environment variable names to secret names. The secret values + will be securely injected as environment variables in the Devbox. Example: + {"DB_PASS": "DATABASE_PASSWORD"} sets environment variable 'DB_PASS' to the + value of secret 'DATABASE_PASSWORD'. + + snapshot_id: Snapshot ID to use for the Devbox. Only one of (Snapshot ID, Blueprint ID, + Blueprint name) should be specified. + + tunnel: (Optional) Configuration for creating a V2 tunnel at Devbox launch time. When + specified, a tunnel will be automatically provisioned and the tunnel details + will be included in the Devbox response. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + return self._post( + "/v1/devboxes", + body=maybe_transform( + { + "blueprint_id": blueprint_id, + "blueprint_name": blueprint_name, + "code_mounts": code_mounts, + "entrypoint": entrypoint, + "environment_variables": environment_variables, + "file_mounts": file_mounts, + "gateways": gateways, + "launch_parameters": launch_parameters, + "mcp": mcp, + "metadata": metadata, + "mounts": mounts, + "name": name, + "repo_connection_id": repo_connection_id, + "secrets": secrets, + "snapshot_id": snapshot_id, + "tunnel": tunnel, + }, + devbox_create_params.DevboxCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxView, + ) + + 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, + ) -> DevboxView: + """ + Get the latest details and status of a Devbox. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/v1/devboxes/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=DevboxView, + ) + + def update( + self, + id: str, + *, + metadata: Optional[Dict[str, str]] | Omit = omit, + name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> DevboxView: + """ + Updates a devbox by doing a complete update the existing name,metadata fields. + It does not patch partial values. + + Args: + metadata: User defined metadata to attach to the devbox for organization. + + name: (Optional) A user specified name to give the Devbox. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/devboxes/{id}", + body=maybe_transform( + { + "metadata": metadata, + "name": name, + }, + devbox_update_params.DevboxUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxView, + ) + + def list( + self, + *, + limit: int | Omit = omit, + starting_after: str | Omit = omit, + status: Literal[ + "provisioning", "initializing", "running", "suspending", "suspended", "resuming", "failure", "shutdown" + ] + | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SyncDevboxesCursorIDPage[DevboxView]: + """ + List all Devboxes while optionally filtering by status. + + Args: + limit: The limit of items to return. Default is 20. Max is 5000. + + starting_after: Load the next page of data starting after the item with the given ID. + + status: Filter by 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( + "/v1/devboxes", + page=SyncDevboxesCursorIDPage[DevboxView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "starting_after": starting_after, + "status": status, + }, + devbox_list_params.DevboxListParams, + ), + ), + model=DevboxView, + ) + + def create_ssh_key( + 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, + idempotency_key: str | None = None, + ) -> DevboxCreateSSHKeyResponse: + """ + Create an SSH key for a Devbox to enable remote access. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/devboxes/{id}/create_ssh_key", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxCreateSSHKeyResponse, + ) + + @typing_extensions.deprecated("deprecated") + def create_tunnel( + self, + id: str, + *, + port: int, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> DevboxTunnelView: + """[Deprecated] Use POST /v1/devboxes/{id}/enable_tunnel instead. + + This endpoint + creates a legacy tunnel. The new enable_tunnel endpoint provides improved tunnel + functionality with authentication options. + + Args: + port: Devbox port that tunnel will expose. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/devboxes/{id}/create_tunnel", + body=maybe_transform({"port": port}, devbox_create_tunnel_params.DevboxCreateTunnelParams), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxTunnelView, + ) + + def delete_disk_snapshot( + 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, + idempotency_key: str | None = None, + ) -> object: + """ + Delete a previously taken disk snapshot of a Devbox. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/devboxes/disk_snapshots/{id}/delete", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=object, + ) + + def download_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, + idempotency_key: str | None = None, + ) -> BinaryAPIResponse: + """ + Download file contents of any type (binary, text, etc) from a specified path on + the Devbox. + + Args: + path: The path on the Devbox filesystem to read the file from. Path is relative to + user home 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 + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: + timeout = 600 + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + return self._post( + f"/v1/devboxes/{id}/download_file", + body=maybe_transform({"path": path}, devbox_download_file_params.DevboxDownloadFileParams), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=BinaryAPIResponse, + ) + + def enable_tunnel( + self, + id: str, + *, + auth_mode: Optional[Literal["open", "authenticated"]] | Omit = omit, + http_keep_alive: Optional[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, + idempotency_key: str | None = None, + ) -> TunnelView: + """Enable a V2 tunnel for an existing running Devbox. + + Tunnels provide encrypted + URL-based access to the Devbox without exposing internal IDs. The tunnel URL + format is: https://{port}-{tunnel_key}.tunnel.runloop.ai + + Each Devbox can have one tunnel. + + Args: + auth_mode: Authentication mode for the tunnel. Defaults to 'public' if not specified. + + http_keep_alive: When true, HTTP traffic through the tunnel counts as activity for idle lifecycle + policies, resetting the idle timer. Defaults to true if not specified. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/devboxes/{id}/enable_tunnel", + body=maybe_transform( + { + "auth_mode": auth_mode, + "http_keep_alive": http_keep_alive, + }, + devbox_enable_tunnel_params.DevboxEnableTunnelParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=TunnelView, + ) + + def execute( + self, + id: str, + *, + command: str, + command_id: str, + last_n: str | Omit = omit, + optimistic_timeout: Optional[int] | Omit = omit, + shell_name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> DevboxAsyncExecutionDetailView: + """ + Execute a command with a known command ID on a devbox, optimistically waiting + for it to complete within the specified timeout. If it completes in time, return + the result. If not, return a status indicating the command is still running. + Note: attach_stdin parameter is not supported; use execute_async for stdin + support. + + Args: + command: The command to execute via the Devbox shell. By default, commands are run from + the user home directory unless shell_name is specified. If shell_name is + specified the command is run from the directory based on the recent state of the + persistent shell. + + command_id: The command ID in UUIDv7 string format for idempotency and tracking + + last_n: Last n lines of standard error / standard out to return (default: 100) + + optimistic_timeout: Timeout in seconds to wait for command completion, up to 25 seconds. Defaults to + 25 seconds. Operation is not killed. + + shell_name: The name of the persistent shell to create or use if already created. When using + a persistent shell, the command will run from the directory at the end of the + previous command and environment variables will be preserved. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: + timeout = 600 + return self._post( + f"/v1/devboxes/{id}/execute", + body=maybe_transform( + { + "command": command, + "command_id": command_id, + "optimistic_timeout": optimistic_timeout, + "shell_name": shell_name, + }, + devbox_execute_params.DevboxExecuteParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + query=maybe_transform({"last_n": last_n}, devbox_execute_params.DevboxExecuteParams), + ), + cast_to=DevboxAsyncExecutionDetailView, + ) + + def execute_async( + self, + id: str, + *, + command: str, + attach_stdin: Optional[bool] | Omit = omit, + shell_name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> DevboxAsyncExecutionDetailView: + """ + Execute the given command in the Devbox shell asynchronously and returns the + execution that can be used to track the command's progress. + + Args: + command: The command to execute via the Devbox shell. By default, commands are run from + the user home directory unless shell_name is specified. If shell_name is + specified the command is run from the directory based on the recent state of the + persistent shell. + + attach_stdin: Whether to attach stdin streaming for async commands. Not valid for execute_sync + endpoint. Defaults to false if not specified. + + shell_name: The name of the persistent shell to create or use if already created. When using + a persistent shell, the command will run from the directory at the end of the + previous command and environment variables will be preserved. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/devboxes/{id}/execute_async", + body=maybe_transform( + { + "command": command, + "attach_stdin": attach_stdin, + "shell_name": shell_name, + }, + devbox_execute_async_params.DevboxExecuteAsyncParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxAsyncExecutionDetailView, + ) + + @typing_extensions.deprecated("deprecated") + def execute_sync( + self, + id: str, + *, + command: str, + attach_stdin: Optional[bool] | Omit = omit, + shell_name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> DevboxExecutionDetailView: + """ + Execute a bash command in the Devbox shell, await the command completion and + return the output. Note: attach_stdin parameter is not supported for synchronous + execution. + + Args: + command: The command to execute via the Devbox shell. By default, commands are run from + the user home directory unless shell_name is specified. If shell_name is + specified the command is run from the directory based on the recent state of the + persistent shell. + + attach_stdin: Whether to attach stdin streaming for async commands. Not valid for execute_sync + endpoint. Defaults to false if not specified. + + shell_name: The name of the persistent shell to create or use if already created. When using + a persistent shell, the command will run from the directory at the end of the + previous command and environment variables will be preserved. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: + timeout = 600 + return self._post( + f"/v1/devboxes/{id}/execute_sync", + body=maybe_transform( + { + "command": command, + "attach_stdin": attach_stdin, + "shell_name": shell_name, + }, + devbox_execute_sync_params.DevboxExecuteSyncParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxExecutionDetailView, + ) + + def keep_alive( + 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, + idempotency_key: str | None = None, + ) -> object: + """ + Send a 'Keep Alive' signal to a running Devbox that is configured to shutdown on + idle so the idle time resets. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/devboxes/{id}/keep_alive", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=object, + ) + + def list_disk_snapshots( + self, + *, + devbox_id: str | Omit = omit, + limit: int | Omit = omit, + metadata_key: str | Omit = omit, + metadata_key_in: str | Omit = omit, + source_blueprint_id: str | Omit = omit, + starting_after: 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, + ) -> SyncDiskSnapshotsCursorIDPage[DevboxSnapshotView]: + """ + List all snapshots of a Devbox while optionally filtering by Devbox ID, source + Blueprint ID, and metadata. + + Args: + devbox_id: Devbox ID to filter by. + + limit: The limit of items to return. Default is 20. Max is 5000. + + metadata_key: Filter snapshots by metadata key-value pair. Can be used multiple times for + different keys. + + metadata_key_in: Filter snapshots by metadata key with multiple possible values (OR condition). + + source_blueprint_id: Source Blueprint ID to filter snapshots by. + + starting_after: Load the next page of data starting after the item with the given ID. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/devboxes/disk_snapshots", + page=SyncDiskSnapshotsCursorIDPage[DevboxSnapshotView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "devbox_id": devbox_id, + "limit": limit, + "metadata_key": metadata_key, + "metadata_key_in": metadata_key_in, + "source_blueprint_id": source_blueprint_id, + "starting_after": starting_after, + }, + devbox_list_disk_snapshots_params.DevboxListDiskSnapshotsParams, + ), + ), + model=DevboxSnapshotView, + ) + + def read_file_contents( + self, + id: str, + *, + file_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, + idempotency_key: str | None = None, + ) -> str: + """Read file contents from a file on a Devbox as a UTF-8. + + Note 'downloadFile' + should be used for large files (greater than 100MB). Returns the file contents + as a UTF-8 string. + + Args: + file_path: The path on the Devbox filesystem to read the file from. Path is relative to + user home 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 + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: + timeout = 600 + extra_headers = {"Accept": "text/plain", **(extra_headers or {})} + return self._post( + f"/v1/devboxes/{id}/read_file_contents", + body=maybe_transform( + {"file_path": file_path}, devbox_read_file_contents_params.DevboxReadFileContentsParams + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=str, + ) + + @typing_extensions.deprecated("deprecated") + def remove_tunnel( + self, + id: str, + *, + port: int, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> object: + """[Deprecated] Tunnels remain active until devbox is shutdown. + + This endpoint + removes a legacy tunnel. + + Args: + port: Devbox port that tunnel will expose. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/devboxes/{id}/remove_tunnel", + body=maybe_transform({"port": port}, devbox_remove_tunnel_params.DevboxRemoveTunnelParams), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=object, + ) + + def resume( + 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, + idempotency_key: str | None = None, + ) -> DevboxView: + """Resume a suspended Devbox with the disk state captured as suspend time. + + Note + that any previously running processes or daemons will need to be restarted using + the Devbox shell tools. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/devboxes/{id}/resume", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxView, + ) + + def retrieve_resource_usage( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> DevboxResourceUsageView: + """Get resource usage metrics for a specific Devbox. + + Returns CPU, memory, and disk + consumption calculated from the Devbox's lifecycle, excluding any suspended + periods for CPU and memory. Disk usage includes the full elapsed time since + storage is consumed even when suspended. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/v1/devboxes/{id}/usage", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=DevboxResourceUsageView, + ) + + def shutdown( + self, + id: str, + *, + force: 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, + idempotency_key: str | None = None, + ) -> DevboxView: + """Shutdown a running Devbox. + + This will permanently stop the Devbox. If you want to + save the state of the Devbox, you should take a snapshot before shutting down or + should suspend the Devbox instead of shutting down. If the Devbox has any + in-progress snapshots, the shutdown will be rejected with a 409 Conflict unless + force=true is specified. + + Args: + force: If true, force shutdown even if snapshots are in progress. Defaults to false. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/devboxes/{id}/shutdown", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + query=maybe_transform({"force": force}, devbox_shutdown_params.DevboxShutdownParams), + ), + cast_to=DevboxView, + ) + + def snapshot_disk( + self, + id: str, + *, + commit_message: Optional[str] | Omit = omit, + metadata: Optional[Dict[str, str]] | Omit = omit, + name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> DevboxSnapshotView: + """ + Create a disk snapshot of a devbox with the specified name and metadata to + enable launching future Devboxes with the same disk state. + + Args: + commit_message: (Optional) Commit message associated with the snapshot (max 1000 characters) + + metadata: (Optional) Metadata used to describe the snapshot + + name: (Optional) A user specified name to give the snapshot + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: + timeout = 600 + return self._post( + f"/v1/devboxes/{id}/snapshot_disk", + body=maybe_transform( + { + "commit_message": commit_message, + "metadata": metadata, + "name": name, + }, + devbox_snapshot_disk_params.DevboxSnapshotDiskParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxSnapshotView, + ) + + def snapshot_disk_async( + self, + id: str, + *, + commit_message: Optional[str] | Omit = omit, + metadata: Optional[Dict[str, str]] | Omit = omit, + name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> DevboxSnapshotView: + """ + Start an asynchronous disk snapshot of a devbox with the specified name and + metadata. The snapshot operation will continue in the background and can be + monitored using the query endpoint. + + Args: + commit_message: (Optional) Commit message associated with the snapshot (max 1000 characters) + + metadata: (Optional) Metadata used to describe the snapshot + + name: (Optional) A user specified name to give the snapshot + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/devboxes/{id}/snapshot_disk_async", + body=maybe_transform( + { + "commit_message": commit_message, + "metadata": metadata, + "name": name, + }, + devbox_snapshot_disk_async_params.DevboxSnapshotDiskAsyncParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxSnapshotView, + ) + + def suspend( + 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, + idempotency_key: str | None = None, + ) -> DevboxView: + """ + Suspend a running Devbox and create a disk snapshot to enable resuming the + Devbox later with the same disk. Note this will not snapshot memory state such + as running processes. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/devboxes/{id}/suspend", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxView, + ) + + def upload_file( + self, + id: str, + *, + path: str, + file: FileTypes | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> object: + """Upload file contents of any type (binary, text, etc) to a Devbox. + + Note this API + is suitable for large files (larger than 100MB) and efficiently uploads files + via multipart form data. + + Args: + path: The path to write the file to on the Devbox. Path is relative to user home + 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 + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: + timeout = 600 + body = deepcopy_minimal( + { + "path": path, + "file": file, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return self._post( + f"/v1/devboxes/{id}/upload_file", + body=maybe_transform(body, devbox_upload_file_params.DevboxUploadFileParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=object, + ) + + def wait_for_command( + self, + execution_id: str, + *, + devbox_id: str, + statuses: List[Literal["queued", "running", "completed"]], + last_n: str | Omit = omit, + timeout_seconds: 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, + idempotency_key: str | None = None, + ) -> DevboxAsyncExecutionDetailView: + """ + Polls the asynchronous execution's status until it reaches one of the desired + statuses or times out. Max is 25 seconds. + + Args: + statuses: The command execution statuses to wait for. At least one status must be + provided. The command will be returned as soon as it reaches any of the provided + statuses. + + last_n: Last n lines of standard error / standard out to return (default: 100) + + timeout_seconds: (Optional) Timeout in seconds to wait for the status, up to 25 seconds. Defaults + to 25 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 + + idempotency_key: Specify a custom idempotency key for this request + """ + if not devbox_id: + raise ValueError(f"Expected a non-empty value for `devbox_id` but received {devbox_id!r}") + if not execution_id: + raise ValueError(f"Expected a non-empty value for `execution_id` but received {execution_id!r}") + return self._post( + f"/v1/devboxes/{devbox_id}/executions/{execution_id}/wait_for_status", + body=maybe_transform( + { + "statuses": statuses, + "timeout_seconds": timeout_seconds, + }, + devbox_wait_for_command_params.DevboxWaitForCommandParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + query=maybe_transform({"last_n": last_n}, devbox_wait_for_command_params.DevboxWaitForCommandParams), + ), + cast_to=DevboxAsyncExecutionDetailView, + ) + + def write_file_contents( + self, + id: str, + *, + contents: str, + file_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, + idempotency_key: str | None = None, + ) -> DevboxExecutionDetailView: + """Write UTF-8 string contents to a file at path on the Devbox. + + Note for large + files (larger than 100MB), the upload_file endpoint must be used. + + Args: + contents: The UTF-8 string contents to write to the file. + + file_path: The path to write the file to on the Devbox. Path is relative to user home + 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 + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: + timeout = 600 + return self._post( + f"/v1/devboxes/{id}/write_file_contents", + body=maybe_transform( + { + "contents": contents, + "file_path": file_path, + }, + devbox_write_file_contents_params.DevboxWriteFileContentsParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxExecutionDetailView, + ) + + +class AsyncDevboxesResource(AsyncAPIResource): + @cached_property + def disk_snapshots(self) -> AsyncDiskSnapshotsResource: + return AsyncDiskSnapshotsResource(self._client) + + @cached_property + def browsers(self) -> AsyncBrowsersResource: + return AsyncBrowsersResource(self._client) + + @cached_property + def computers(self) -> AsyncComputersResource: + return AsyncComputersResource(self._client) + + @cached_property + def logs(self) -> AsyncLogsResource: + return AsyncLogsResource(self._client) + + @cached_property + def executions(self) -> AsyncExecutionsResource: + return AsyncExecutionsResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncDevboxesResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return AsyncDevboxesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncDevboxesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return AsyncDevboxesResourceWithStreamingResponse(self) + + async def create( + self, + *, + blueprint_id: Optional[str] | Omit = omit, + blueprint_name: Optional[str] | Omit = omit, + code_mounts: Optional[Iterable[CodeMountParameters]] | Omit = omit, + entrypoint: Optional[str] | Omit = omit, + environment_variables: Optional[Dict[str, str]] | Omit = omit, + file_mounts: Optional[Dict[str, str]] | Omit = omit, + gateways: Optional[Dict[str, devbox_create_params.Gateways]] | Omit = omit, + launch_parameters: Optional[LaunchParameters] | Omit = omit, + mcp: Optional[Dict[str, devbox_create_params.Mcp]] | Omit = omit, + metadata: Optional[Dict[str, str]] | Omit = omit, + mounts: Optional[Iterable[Mount]] | Omit = omit, + name: Optional[str] | Omit = omit, + repo_connection_id: Optional[str] | Omit = omit, + secrets: Optional[Dict[str, str]] | Omit = omit, + snapshot_id: Optional[str] | Omit = omit, + tunnel: Optional[devbox_create_params.Tunnel] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> DevboxView: + """Create a Devbox and begin the boot process. + + The Devbox will initially launch in + the 'provisioning' state while Runloop allocates the necessary infrastructure. + It will transition to the 'initializing' state while the booted Devbox runs any + Runloop or user defined set up scripts. Finally, the Devbox will transition to + the 'running' state when it is ready for use. + + Args: + blueprint_id: Blueprint ID to use for the Devbox. If none set, the Devbox will be created with + the default Runloop Devbox image. Only one of (Snapshot ID, Blueprint ID, + Blueprint name) should be specified. + + blueprint_name: Name of Blueprint to use for the Devbox. When set, this will load the latest + successfully built Blueprint with the given name. Only one of (Snapshot ID, + Blueprint ID, Blueprint name) should be specified. + + code_mounts: A list of code mounts to be included in the Devbox. Use mounts instead. + + entrypoint: (Optional) When specified, the Devbox will run this script as its main + executable. The devbox lifecycle will be bound to entrypoint, shutting down when + the process is complete. + + environment_variables: (Optional) Environment variables used to configure your Devbox. + + file_mounts: Map of paths and file contents to write before setup. Use mounts instead. + + gateways: (Optional) Agent gateway specifications for credential proxying. Map key is the + environment variable prefix (e.g., 'GWS_ANTHROPIC'). The agent gateway will + proxy requests to external APIs using the specified credential without exposing + the real API key. Example: {'GWS_ANTHROPIC': {'gateway': 'anthropic', 'secret': + 'my_claude_key'}} + + launch_parameters: Parameters to configure the resources and launch time behavior of the Devbox. + + mcp: [Beta] (Optional) MCP specifications for MCP server access. Map key is the + environment variable name for the MCP token envelope. Each spec links an MCP + config to a secret. The devbox will also receive RL_MCP_URL for the MCP hub + endpoint. Example: {'MCP_SECRET': {'mcp_config': 'github-readonly', 'secret': + 'MY_GITHUB_TOKEN'}} + + metadata: User defined metadata to attach to the devbox for organization. + + mounts: A list of mounts to be included in the Devbox. + + name: (Optional) A user specified name to give the Devbox. + + repo_connection_id: Repository connection id the devbox should source its base image from. + + secrets: (Optional) Map of environment variable names to secret names. The secret values + will be securely injected as environment variables in the Devbox. Example: + {"DB_PASS": "DATABASE_PASSWORD"} sets environment variable 'DB_PASS' to the + value of secret 'DATABASE_PASSWORD'. + + snapshot_id: Snapshot ID to use for the Devbox. Only one of (Snapshot ID, Blueprint ID, + Blueprint name) should be specified. + + tunnel: (Optional) Configuration for creating a V2 tunnel at Devbox launch time. When + specified, a tunnel will be automatically provisioned and the tunnel details + will be included in the Devbox response. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + return await self._post( + "/v1/devboxes", + body=await async_maybe_transform( + { + "blueprint_id": blueprint_id, + "blueprint_name": blueprint_name, + "code_mounts": code_mounts, + "entrypoint": entrypoint, + "environment_variables": environment_variables, + "file_mounts": file_mounts, + "gateways": gateways, + "launch_parameters": launch_parameters, + "mcp": mcp, + "metadata": metadata, + "mounts": mounts, + "name": name, + "repo_connection_id": repo_connection_id, + "secrets": secrets, + "snapshot_id": snapshot_id, + "tunnel": tunnel, + }, + devbox_create_params.DevboxCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxView, + ) + + 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, + ) -> DevboxView: + """ + Get the latest details and status of a Devbox. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/v1/devboxes/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=DevboxView, + ) + + async def update( + self, + id: str, + *, + metadata: Optional[Dict[str, str]] | Omit = omit, + name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> DevboxView: + """ + Updates a devbox by doing a complete update the existing name,metadata fields. + It does not patch partial values. + + Args: + metadata: User defined metadata to attach to the devbox for organization. + + name: (Optional) A user specified name to give the Devbox. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/devboxes/{id}", + body=await async_maybe_transform( + { + "metadata": metadata, + "name": name, + }, + devbox_update_params.DevboxUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxView, + ) + + def list( + self, + *, + limit: int | Omit = omit, + starting_after: str | Omit = omit, + status: Literal[ + "provisioning", "initializing", "running", "suspending", "suspended", "resuming", "failure", "shutdown" + ] + | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this 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[DevboxView, AsyncDevboxesCursorIDPage[DevboxView]]: + """ + List all Devboxes while optionally filtering by status. + + Args: + limit: The limit of items to return. Default is 20. Max is 5000. + + starting_after: Load the next page of data starting after the item with the given ID. + + status: Filter by 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( + "/v1/devboxes", + page=AsyncDevboxesCursorIDPage[DevboxView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "starting_after": starting_after, + "status": status, + }, + devbox_list_params.DevboxListParams, + ), + ), + model=DevboxView, + ) + + async def create_ssh_key( + 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, + idempotency_key: str | None = None, + ) -> DevboxCreateSSHKeyResponse: + """ + Create an SSH key for a Devbox to enable remote access. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/devboxes/{id}/create_ssh_key", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxCreateSSHKeyResponse, + ) + + @typing_extensions.deprecated("deprecated") + async def create_tunnel( + self, + id: str, + *, + port: int, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> DevboxTunnelView: + """[Deprecated] Use POST /v1/devboxes/{id}/enable_tunnel instead. + + This endpoint + creates a legacy tunnel. The new enable_tunnel endpoint provides improved tunnel + functionality with authentication options. + + Args: + port: Devbox port that tunnel will expose. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/devboxes/{id}/create_tunnel", + body=await async_maybe_transform({"port": port}, devbox_create_tunnel_params.DevboxCreateTunnelParams), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxTunnelView, + ) + + async def delete_disk_snapshot( + 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, + idempotency_key: str | None = None, + ) -> object: + """ + Delete a previously taken disk snapshot of a Devbox. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/devboxes/disk_snapshots/{id}/delete", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=object, + ) + + async def download_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, + idempotency_key: str | None = None, + ) -> AsyncBinaryAPIResponse: + """ + Download file contents of any type (binary, text, etc) from a specified path on + the Devbox. + + Args: + path: The path on the Devbox filesystem to read the file from. Path is relative to + user home 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 + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: + timeout = 600 + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + return await self._post( + f"/v1/devboxes/{id}/download_file", + body=await async_maybe_transform({"path": path}, devbox_download_file_params.DevboxDownloadFileParams), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=AsyncBinaryAPIResponse, + ) + + async def enable_tunnel( + self, + id: str, + *, + auth_mode: Optional[Literal["open", "authenticated"]] | Omit = omit, + http_keep_alive: Optional[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, + idempotency_key: str | None = None, + ) -> TunnelView: + """Enable a V2 tunnel for an existing running Devbox. + + Tunnels provide encrypted + URL-based access to the Devbox without exposing internal IDs. The tunnel URL + format is: https://{port}-{tunnel_key}.tunnel.runloop.ai + + Each Devbox can have one tunnel. + + Args: + auth_mode: Authentication mode for the tunnel. Defaults to 'public' if not specified. + + http_keep_alive: When true, HTTP traffic through the tunnel counts as activity for idle lifecycle + policies, resetting the idle timer. Defaults to true if not specified. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/devboxes/{id}/enable_tunnel", + body=await async_maybe_transform( + { + "auth_mode": auth_mode, + "http_keep_alive": http_keep_alive, + }, + devbox_enable_tunnel_params.DevboxEnableTunnelParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=TunnelView, + ) + + async def execute( + self, + id: str, + *, + command: str, + command_id: str, + last_n: str | Omit = omit, + optimistic_timeout: Optional[int] | Omit = omit, + shell_name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> DevboxAsyncExecutionDetailView: + """ + Execute a command with a known command ID on a devbox, optimistically waiting + for it to complete within the specified timeout. If it completes in time, return + the result. If not, return a status indicating the command is still running. + Note: attach_stdin parameter is not supported; use execute_async for stdin + support. + + Args: + command: The command to execute via the Devbox shell. By default, commands are run from + the user home directory unless shell_name is specified. If shell_name is + specified the command is run from the directory based on the recent state of the + persistent shell. + + command_id: The command ID in UUIDv7 string format for idempotency and tracking + + last_n: Last n lines of standard error / standard out to return (default: 100) + + optimistic_timeout: Timeout in seconds to wait for command completion, up to 25 seconds. Defaults to + 25 seconds. Operation is not killed. + + shell_name: The name of the persistent shell to create or use if already created. When using + a persistent shell, the command will run from the directory at the end of the + previous command and environment variables will be preserved. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: + timeout = 600 + return await self._post( + f"/v1/devboxes/{id}/execute", + body=await async_maybe_transform( + { + "command": command, + "command_id": command_id, + "optimistic_timeout": optimistic_timeout, + "shell_name": shell_name, + }, + devbox_execute_params.DevboxExecuteParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + query=await async_maybe_transform({"last_n": last_n}, devbox_execute_params.DevboxExecuteParams), + ), + cast_to=DevboxAsyncExecutionDetailView, + ) + + async def execute_async( + self, + id: str, + *, + command: str, + attach_stdin: Optional[bool] | Omit = omit, + shell_name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> DevboxAsyncExecutionDetailView: + """ + Execute the given command in the Devbox shell asynchronously and returns the + execution that can be used to track the command's progress. + + Args: + command: The command to execute via the Devbox shell. By default, commands are run from + the user home directory unless shell_name is specified. If shell_name is + specified the command is run from the directory based on the recent state of the + persistent shell. + + attach_stdin: Whether to attach stdin streaming for async commands. Not valid for execute_sync + endpoint. Defaults to false if not specified. + + shell_name: The name of the persistent shell to create or use if already created. When using + a persistent shell, the command will run from the directory at the end of the + previous command and environment variables will be preserved. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/devboxes/{id}/execute_async", + body=await async_maybe_transform( + { + "command": command, + "attach_stdin": attach_stdin, + "shell_name": shell_name, + }, + devbox_execute_async_params.DevboxExecuteAsyncParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxAsyncExecutionDetailView, + ) + + @typing_extensions.deprecated("deprecated") + async def execute_sync( + self, + id: str, + *, + command: str, + attach_stdin: Optional[bool] | Omit = omit, + shell_name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> DevboxExecutionDetailView: + """ + Execute a bash command in the Devbox shell, await the command completion and + return the output. Note: attach_stdin parameter is not supported for synchronous + execution. + + Args: + command: The command to execute via the Devbox shell. By default, commands are run from + the user home directory unless shell_name is specified. If shell_name is + specified the command is run from the directory based on the recent state of the + persistent shell. + + attach_stdin: Whether to attach stdin streaming for async commands. Not valid for execute_sync + endpoint. Defaults to false if not specified. + + shell_name: The name of the persistent shell to create or use if already created. When using + a persistent shell, the command will run from the directory at the end of the + previous command and environment variables will be preserved. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: + timeout = 600 + return await self._post( + f"/v1/devboxes/{id}/execute_sync", + body=await async_maybe_transform( + { + "command": command, + "attach_stdin": attach_stdin, + "shell_name": shell_name, + }, + devbox_execute_sync_params.DevboxExecuteSyncParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxExecutionDetailView, + ) + + async def keep_alive( + 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, + idempotency_key: str | None = None, + ) -> object: + """ + Send a 'Keep Alive' signal to a running Devbox that is configured to shutdown on + idle so the idle time resets. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/devboxes/{id}/keep_alive", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=object, + ) + + def list_disk_snapshots( + self, + *, + devbox_id: str | Omit = omit, + limit: int | Omit = omit, + metadata_key: str | Omit = omit, + metadata_key_in: str | Omit = omit, + source_blueprint_id: str | Omit = omit, + starting_after: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[DevboxSnapshotView, AsyncDiskSnapshotsCursorIDPage[DevboxSnapshotView]]: + """ + List all snapshots of a Devbox while optionally filtering by Devbox ID, source + Blueprint ID, and metadata. + + Args: + devbox_id: Devbox ID to filter by. + + limit: The limit of items to return. Default is 20. Max is 5000. + + metadata_key: Filter snapshots by metadata key-value pair. Can be used multiple times for + different keys. + + metadata_key_in: Filter snapshots by metadata key with multiple possible values (OR condition). + + source_blueprint_id: Source Blueprint ID to filter snapshots by. + + starting_after: Load the next page of data starting after the item with the given ID. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/devboxes/disk_snapshots", + page=AsyncDiskSnapshotsCursorIDPage[DevboxSnapshotView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "devbox_id": devbox_id, + "limit": limit, + "metadata_key": metadata_key, + "metadata_key_in": metadata_key_in, + "source_blueprint_id": source_blueprint_id, + "starting_after": starting_after, + }, + devbox_list_disk_snapshots_params.DevboxListDiskSnapshotsParams, + ), + ), + model=DevboxSnapshotView, + ) + + async def read_file_contents( + self, + id: str, + *, + file_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, + idempotency_key: str | None = None, + ) -> str: + """Read file contents from a file on a Devbox as a UTF-8. + + Note 'downloadFile' + should be used for large files (greater than 100MB). Returns the file contents + as a UTF-8 string. + + Args: + file_path: The path on the Devbox filesystem to read the file from. Path is relative to + user home 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 + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: + timeout = 600 + extra_headers = {"Accept": "text/plain", **(extra_headers or {})} + return await self._post( + f"/v1/devboxes/{id}/read_file_contents", + body=await async_maybe_transform( + {"file_path": file_path}, devbox_read_file_contents_params.DevboxReadFileContentsParams + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=str, + ) + + @typing_extensions.deprecated("deprecated") + async def remove_tunnel( + self, + id: str, + *, + port: int, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> object: + """[Deprecated] Tunnels remain active until devbox is shutdown. + + This endpoint + removes a legacy tunnel. + + Args: + port: Devbox port that tunnel will expose. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/devboxes/{id}/remove_tunnel", + body=await async_maybe_transform({"port": port}, devbox_remove_tunnel_params.DevboxRemoveTunnelParams), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=object, + ) + + async def resume( + 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, + idempotency_key: str | None = None, + ) -> DevboxView: + """Resume a suspended Devbox with the disk state captured as suspend time. + + Note + that any previously running processes or daemons will need to be restarted using + the Devbox shell tools. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/devboxes/{id}/resume", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxView, + ) + + async def retrieve_resource_usage( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> DevboxResourceUsageView: + """Get resource usage metrics for a specific Devbox. + + Returns CPU, memory, and disk + consumption calculated from the Devbox's lifecycle, excluding any suspended + periods for CPU and memory. Disk usage includes the full elapsed time since + storage is consumed even when suspended. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/v1/devboxes/{id}/usage", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=DevboxResourceUsageView, + ) + + async def shutdown( + self, + id: str, + *, + force: 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, + idempotency_key: str | None = None, + ) -> DevboxView: + """Shutdown a running Devbox. + + This will permanently stop the Devbox. If you want to + save the state of the Devbox, you should take a snapshot before shutting down or + should suspend the Devbox instead of shutting down. If the Devbox has any + in-progress snapshots, the shutdown will be rejected with a 409 Conflict unless + force=true is specified. + + Args: + force: If true, force shutdown even if snapshots are in progress. Defaults to false. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/devboxes/{id}/shutdown", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + query=await async_maybe_transform({"force": force}, devbox_shutdown_params.DevboxShutdownParams), + ), + cast_to=DevboxView, + ) + + async def snapshot_disk( + self, + id: str, + *, + commit_message: Optional[str] | Omit = omit, + metadata: Optional[Dict[str, str]] | Omit = omit, + name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> DevboxSnapshotView: + """ + Create a disk snapshot of a devbox with the specified name and metadata to + enable launching future Devboxes with the same disk state. + + Args: + commit_message: (Optional) Commit message associated with the snapshot (max 1000 characters) + + metadata: (Optional) Metadata used to describe the snapshot + + name: (Optional) A user specified name to give the snapshot + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: + timeout = 600 + return await self._post( + f"/v1/devboxes/{id}/snapshot_disk", + body=await async_maybe_transform( + { + "commit_message": commit_message, + "metadata": metadata, + "name": name, + }, + devbox_snapshot_disk_params.DevboxSnapshotDiskParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxSnapshotView, + ) + + async def snapshot_disk_async( + self, + id: str, + *, + commit_message: Optional[str] | Omit = omit, + metadata: Optional[Dict[str, str]] | Omit = omit, + name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> DevboxSnapshotView: + """ + Start an asynchronous disk snapshot of a devbox with the specified name and + metadata. The snapshot operation will continue in the background and can be + monitored using the query endpoint. + + Args: + commit_message: (Optional) Commit message associated with the snapshot (max 1000 characters) + + metadata: (Optional) Metadata used to describe the snapshot + + name: (Optional) A user specified name to give the snapshot + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/devboxes/{id}/snapshot_disk_async", + body=await async_maybe_transform( + { + "commit_message": commit_message, + "metadata": metadata, + "name": name, + }, + devbox_snapshot_disk_async_params.DevboxSnapshotDiskAsyncParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxSnapshotView, + ) + + async def suspend( + 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, + idempotency_key: str | None = None, + ) -> DevboxView: + """ + Suspend a running Devbox and create a disk snapshot to enable resuming the + Devbox later with the same disk. Note this will not snapshot memory state such + as running processes. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/devboxes/{id}/suspend", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxView, + ) + + async def upload_file( + self, + id: str, + *, + path: str, + file: FileTypes | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> object: + """Upload file contents of any type (binary, text, etc) to a Devbox. + + Note this API + is suitable for large files (larger than 100MB) and efficiently uploads files + via multipart form data. + + Args: + path: The path to write the file to on the Devbox. Path is relative to user home + 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 + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: + timeout = 600 + body = deepcopy_minimal( + { + "path": path, + "file": file, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return await self._post( + f"/v1/devboxes/{id}/upload_file", + body=await async_maybe_transform(body, devbox_upload_file_params.DevboxUploadFileParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=object, + ) + + async def wait_for_command( + self, + execution_id: str, + *, + devbox_id: str, + statuses: List[Literal["queued", "running", "completed"]], + last_n: str | Omit = omit, + timeout_seconds: 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, + idempotency_key: str | None = None, + ) -> DevboxAsyncExecutionDetailView: + """ + Polls the asynchronous execution's status until it reaches one of the desired + statuses or times out. Max is 25 seconds. + + Args: + statuses: The command execution statuses to wait for. At least one status must be + provided. The command will be returned as soon as it reaches any of the provided + statuses. + + last_n: Last n lines of standard error / standard out to return (default: 100) + + timeout_seconds: (Optional) Timeout in seconds to wait for the status, up to 25 seconds. Defaults + to 25 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 + + idempotency_key: Specify a custom idempotency key for this request + """ + if not devbox_id: + raise ValueError(f"Expected a non-empty value for `devbox_id` but received {devbox_id!r}") + if not execution_id: + raise ValueError(f"Expected a non-empty value for `execution_id` but received {execution_id!r}") + return await self._post( + f"/v1/devboxes/{devbox_id}/executions/{execution_id}/wait_for_status", + body=await async_maybe_transform( + { + "statuses": statuses, + "timeout_seconds": timeout_seconds, + }, + devbox_wait_for_command_params.DevboxWaitForCommandParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + query=await async_maybe_transform( + {"last_n": last_n}, devbox_wait_for_command_params.DevboxWaitForCommandParams + ), + ), + cast_to=DevboxAsyncExecutionDetailView, + ) + + async def write_file_contents( + self, + id: str, + *, + contents: str, + file_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, + idempotency_key: str | None = None, + ) -> DevboxExecutionDetailView: + """Write UTF-8 string contents to a file at path on the Devbox. + + Note for large + files (larger than 100MB), the upload_file endpoint must be used. + + Args: + contents: The UTF-8 string contents to write to the file. + + file_path: The path to write the file to on the Devbox. Path is relative to user home + 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 + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: + timeout = 600 + return await self._post( + f"/v1/devboxes/{id}/write_file_contents", + body=await async_maybe_transform( + { + "contents": contents, + "file_path": file_path, + }, + devbox_write_file_contents_params.DevboxWriteFileContentsParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxExecutionDetailView, + ) + + +class DevboxesResourceWithRawResponse: + def __init__(self, devboxes: DevboxesResource) -> None: + self._devboxes = devboxes + + self.create = to_raw_response_wrapper( + devboxes.create, + ) + self.retrieve = to_raw_response_wrapper( + devboxes.retrieve, + ) + self.update = to_raw_response_wrapper( + devboxes.update, + ) + self.list = to_raw_response_wrapper( + devboxes.list, + ) + self.create_ssh_key = to_raw_response_wrapper( + devboxes.create_ssh_key, + ) + self.create_tunnel = ( # pyright: ignore[reportDeprecated] + to_raw_response_wrapper( + devboxes.create_tunnel, # pyright: ignore[reportDeprecated], + ) + ) + self.delete_disk_snapshot = to_raw_response_wrapper( + devboxes.delete_disk_snapshot, + ) + self.download_file = to_custom_raw_response_wrapper( + devboxes.download_file, + BinaryAPIResponse, + ) + self.enable_tunnel = to_raw_response_wrapper( + devboxes.enable_tunnel, + ) + self.execute = to_raw_response_wrapper( + devboxes.execute, + ) + self.execute_async = to_raw_response_wrapper( + devboxes.execute_async, + ) + self.execute_sync = ( # pyright: ignore[reportDeprecated] + to_raw_response_wrapper( + devboxes.execute_sync, # pyright: ignore[reportDeprecated], + ) + ) + self.keep_alive = to_raw_response_wrapper( + devboxes.keep_alive, + ) + self.list_disk_snapshots = to_raw_response_wrapper( + devboxes.list_disk_snapshots, + ) + self.read_file_contents = to_raw_response_wrapper( + devboxes.read_file_contents, + ) + self.remove_tunnel = ( # pyright: ignore[reportDeprecated] + to_raw_response_wrapper( + devboxes.remove_tunnel, # pyright: ignore[reportDeprecated], + ) + ) + self.resume = to_raw_response_wrapper( + devboxes.resume, + ) + self.retrieve_resource_usage = to_raw_response_wrapper( + devboxes.retrieve_resource_usage, + ) + self.shutdown = to_raw_response_wrapper( + devboxes.shutdown, + ) + self.snapshot_disk = to_raw_response_wrapper( + devboxes.snapshot_disk, + ) + self.snapshot_disk_async = to_raw_response_wrapper( + devboxes.snapshot_disk_async, + ) + self.suspend = to_raw_response_wrapper( + devboxes.suspend, + ) + self.upload_file = to_raw_response_wrapper( + devboxes.upload_file, + ) + self.wait_for_command = to_raw_response_wrapper( + devboxes.wait_for_command, + ) + self.write_file_contents = to_raw_response_wrapper( + devboxes.write_file_contents, + ) + + @cached_property + def disk_snapshots(self) -> DiskSnapshotsResourceWithRawResponse: + return DiskSnapshotsResourceWithRawResponse(self._devboxes.disk_snapshots) + + @cached_property + def browsers(self) -> BrowsersResourceWithRawResponse: + return BrowsersResourceWithRawResponse(self._devboxes.browsers) + + @cached_property + def computers(self) -> ComputersResourceWithRawResponse: + return ComputersResourceWithRawResponse(self._devboxes.computers) + + @cached_property + def logs(self) -> LogsResourceWithRawResponse: + return LogsResourceWithRawResponse(self._devboxes.logs) + + @cached_property + def executions(self) -> ExecutionsResourceWithRawResponse: + return ExecutionsResourceWithRawResponse(self._devboxes.executions) + + +class AsyncDevboxesResourceWithRawResponse: + def __init__(self, devboxes: AsyncDevboxesResource) -> None: + self._devboxes = devboxes + + self.create = async_to_raw_response_wrapper( + devboxes.create, + ) + self.retrieve = async_to_raw_response_wrapper( + devboxes.retrieve, + ) + self.update = async_to_raw_response_wrapper( + devboxes.update, + ) + self.list = async_to_raw_response_wrapper( + devboxes.list, + ) + self.create_ssh_key = async_to_raw_response_wrapper( + devboxes.create_ssh_key, + ) + self.create_tunnel = ( # pyright: ignore[reportDeprecated] + async_to_raw_response_wrapper( + devboxes.create_tunnel, # pyright: ignore[reportDeprecated], + ) + ) + self.delete_disk_snapshot = async_to_raw_response_wrapper( + devboxes.delete_disk_snapshot, + ) + self.download_file = async_to_custom_raw_response_wrapper( + devboxes.download_file, + AsyncBinaryAPIResponse, + ) + self.enable_tunnel = async_to_raw_response_wrapper( + devboxes.enable_tunnel, + ) + self.execute = async_to_raw_response_wrapper( + devboxes.execute, + ) + self.execute_async = async_to_raw_response_wrapper( + devboxes.execute_async, + ) + self.execute_sync = ( # pyright: ignore[reportDeprecated] + async_to_raw_response_wrapper( + devboxes.execute_sync, # pyright: ignore[reportDeprecated], + ) + ) + self.keep_alive = async_to_raw_response_wrapper( + devboxes.keep_alive, + ) + self.list_disk_snapshots = async_to_raw_response_wrapper( + devboxes.list_disk_snapshots, + ) + self.read_file_contents = async_to_raw_response_wrapper( + devboxes.read_file_contents, + ) + self.remove_tunnel = ( # pyright: ignore[reportDeprecated] + async_to_raw_response_wrapper( + devboxes.remove_tunnel, # pyright: ignore[reportDeprecated], + ) + ) + self.resume = async_to_raw_response_wrapper( + devboxes.resume, + ) + self.retrieve_resource_usage = async_to_raw_response_wrapper( + devboxes.retrieve_resource_usage, + ) + self.shutdown = async_to_raw_response_wrapper( + devboxes.shutdown, + ) + self.snapshot_disk = async_to_raw_response_wrapper( + devboxes.snapshot_disk, + ) + self.snapshot_disk_async = async_to_raw_response_wrapper( + devboxes.snapshot_disk_async, + ) + self.suspend = async_to_raw_response_wrapper( + devboxes.suspend, + ) + self.upload_file = async_to_raw_response_wrapper( + devboxes.upload_file, + ) + self.wait_for_command = async_to_raw_response_wrapper( + devboxes.wait_for_command, + ) + self.write_file_contents = async_to_raw_response_wrapper( + devboxes.write_file_contents, + ) + + @cached_property + def disk_snapshots(self) -> AsyncDiskSnapshotsResourceWithRawResponse: + return AsyncDiskSnapshotsResourceWithRawResponse(self._devboxes.disk_snapshots) + + @cached_property + def browsers(self) -> AsyncBrowsersResourceWithRawResponse: + return AsyncBrowsersResourceWithRawResponse(self._devboxes.browsers) + + @cached_property + def computers(self) -> AsyncComputersResourceWithRawResponse: + return AsyncComputersResourceWithRawResponse(self._devboxes.computers) + + @cached_property + def logs(self) -> AsyncLogsResourceWithRawResponse: + return AsyncLogsResourceWithRawResponse(self._devboxes.logs) + + @cached_property + def executions(self) -> AsyncExecutionsResourceWithRawResponse: + return AsyncExecutionsResourceWithRawResponse(self._devboxes.executions) + + +class DevboxesResourceWithStreamingResponse: + def __init__(self, devboxes: DevboxesResource) -> None: + self._devboxes = devboxes + + self.create = to_streamed_response_wrapper( + devboxes.create, + ) + self.retrieve = to_streamed_response_wrapper( + devboxes.retrieve, + ) + self.update = to_streamed_response_wrapper( + devboxes.update, + ) + self.list = to_streamed_response_wrapper( + devboxes.list, + ) + self.create_ssh_key = to_streamed_response_wrapper( + devboxes.create_ssh_key, + ) + self.create_tunnel = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + devboxes.create_tunnel, # pyright: ignore[reportDeprecated], + ) + ) + self.delete_disk_snapshot = to_streamed_response_wrapper( + devboxes.delete_disk_snapshot, + ) + self.download_file = to_custom_streamed_response_wrapper( + devboxes.download_file, + StreamedBinaryAPIResponse, + ) + self.enable_tunnel = to_streamed_response_wrapper( + devboxes.enable_tunnel, + ) + self.execute = to_streamed_response_wrapper( + devboxes.execute, + ) + self.execute_async = to_streamed_response_wrapper( + devboxes.execute_async, + ) + self.execute_sync = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + devboxes.execute_sync, # pyright: ignore[reportDeprecated], + ) + ) + self.keep_alive = to_streamed_response_wrapper( + devboxes.keep_alive, + ) + self.list_disk_snapshots = to_streamed_response_wrapper( + devboxes.list_disk_snapshots, + ) + self.read_file_contents = to_streamed_response_wrapper( + devboxes.read_file_contents, + ) + self.remove_tunnel = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + devboxes.remove_tunnel, # pyright: ignore[reportDeprecated], + ) + ) + self.resume = to_streamed_response_wrapper( + devboxes.resume, + ) + self.retrieve_resource_usage = to_streamed_response_wrapper( + devboxes.retrieve_resource_usage, + ) + self.shutdown = to_streamed_response_wrapper( + devboxes.shutdown, + ) + self.snapshot_disk = to_streamed_response_wrapper( + devboxes.snapshot_disk, + ) + self.snapshot_disk_async = to_streamed_response_wrapper( + devboxes.snapshot_disk_async, + ) + self.suspend = to_streamed_response_wrapper( + devboxes.suspend, + ) + self.upload_file = to_streamed_response_wrapper( + devboxes.upload_file, + ) + self.wait_for_command = to_streamed_response_wrapper( + devboxes.wait_for_command, + ) + self.write_file_contents = to_streamed_response_wrapper( + devboxes.write_file_contents, + ) + + @cached_property + def disk_snapshots(self) -> DiskSnapshotsResourceWithStreamingResponse: + return DiskSnapshotsResourceWithStreamingResponse(self._devboxes.disk_snapshots) + + @cached_property + def browsers(self) -> BrowsersResourceWithStreamingResponse: + return BrowsersResourceWithStreamingResponse(self._devboxes.browsers) + + @cached_property + def computers(self) -> ComputersResourceWithStreamingResponse: + return ComputersResourceWithStreamingResponse(self._devboxes.computers) + + @cached_property + def logs(self) -> LogsResourceWithStreamingResponse: + return LogsResourceWithStreamingResponse(self._devboxes.logs) + + @cached_property + def executions(self) -> ExecutionsResourceWithStreamingResponse: + return ExecutionsResourceWithStreamingResponse(self._devboxes.executions) + + +class AsyncDevboxesResourceWithStreamingResponse: + def __init__(self, devboxes: AsyncDevboxesResource) -> None: + self._devboxes = devboxes + + self.create = async_to_streamed_response_wrapper( + devboxes.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + devboxes.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + devboxes.update, + ) + self.list = async_to_streamed_response_wrapper( + devboxes.list, + ) + self.create_ssh_key = async_to_streamed_response_wrapper( + devboxes.create_ssh_key, + ) + self.create_tunnel = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + devboxes.create_tunnel, # pyright: ignore[reportDeprecated], + ) + ) + self.delete_disk_snapshot = async_to_streamed_response_wrapper( + devboxes.delete_disk_snapshot, + ) + self.download_file = async_to_custom_streamed_response_wrapper( + devboxes.download_file, + AsyncStreamedBinaryAPIResponse, + ) + self.enable_tunnel = async_to_streamed_response_wrapper( + devboxes.enable_tunnel, + ) + self.execute = async_to_streamed_response_wrapper( + devboxes.execute, + ) + self.execute_async = async_to_streamed_response_wrapper( + devboxes.execute_async, + ) + self.execute_sync = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + devboxes.execute_sync, # pyright: ignore[reportDeprecated], + ) + ) + self.keep_alive = async_to_streamed_response_wrapper( + devboxes.keep_alive, + ) + self.list_disk_snapshots = async_to_streamed_response_wrapper( + devboxes.list_disk_snapshots, + ) + self.read_file_contents = async_to_streamed_response_wrapper( + devboxes.read_file_contents, + ) + self.remove_tunnel = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + devboxes.remove_tunnel, # pyright: ignore[reportDeprecated], + ) + ) + self.resume = async_to_streamed_response_wrapper( + devboxes.resume, + ) + self.retrieve_resource_usage = async_to_streamed_response_wrapper( + devboxes.retrieve_resource_usage, + ) + self.shutdown = async_to_streamed_response_wrapper( + devboxes.shutdown, + ) + self.snapshot_disk = async_to_streamed_response_wrapper( + devboxes.snapshot_disk, + ) + self.snapshot_disk_async = async_to_streamed_response_wrapper( + devboxes.snapshot_disk_async, + ) + self.suspend = async_to_streamed_response_wrapper( + devboxes.suspend, + ) + self.upload_file = async_to_streamed_response_wrapper( + devboxes.upload_file, + ) + self.wait_for_command = async_to_streamed_response_wrapper( + devboxes.wait_for_command, + ) + self.write_file_contents = async_to_streamed_response_wrapper( + devboxes.write_file_contents, + ) + + @cached_property + def disk_snapshots(self) -> AsyncDiskSnapshotsResourceWithStreamingResponse: + return AsyncDiskSnapshotsResourceWithStreamingResponse(self._devboxes.disk_snapshots) + + @cached_property + def browsers(self) -> AsyncBrowsersResourceWithStreamingResponse: + return AsyncBrowsersResourceWithStreamingResponse(self._devboxes.browsers) + + @cached_property + def computers(self) -> AsyncComputersResourceWithStreamingResponse: + return AsyncComputersResourceWithStreamingResponse(self._devboxes.computers) + + @cached_property + def logs(self) -> AsyncLogsResourceWithStreamingResponse: + return AsyncLogsResourceWithStreamingResponse(self._devboxes.logs) + + @cached_property + def executions(self) -> AsyncExecutionsResourceWithStreamingResponse: + return AsyncExecutionsResourceWithStreamingResponse(self._devboxes.executions) diff --git a/src/runloop_api_client/resources/devboxes/disk_snapshots.py b/src/runloop_api_client/resources/devboxes/disk_snapshots.py new file mode 100644 index 000000000..b66186680 --- /dev/null +++ b/src/runloop_api_client/resources/devboxes/disk_snapshots.py @@ -0,0 +1,535 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, Optional + +import httpx + +from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from ..._utils import maybe_transform, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ...pagination import SyncDiskSnapshotsCursorIDPage, AsyncDiskSnapshotsCursorIDPage +from ..._base_client import AsyncPaginator, make_request_options +from ...types.devboxes import disk_snapshot_list_params, disk_snapshot_update_params +from ...types.devbox_snapshot_view import DevboxSnapshotView +from ...types.devboxes.devbox_snapshot_async_status_view import DevboxSnapshotAsyncStatusView + +__all__ = ["DiskSnapshotsResource", "AsyncDiskSnapshotsResource"] + + +class DiskSnapshotsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> DiskSnapshotsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return DiskSnapshotsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> DiskSnapshotsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return DiskSnapshotsResourceWithStreamingResponse(self) + + def update( + self, + id: str, + *, + commit_message: Optional[str] | Omit = omit, + metadata: Optional[Dict[str, str]] | Omit = omit, + name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> DevboxSnapshotView: + """Updates disk snapshot metadata via update vs patch. + + The entire metadata will be + replaced. + + Args: + commit_message: (Optional) Commit message associated with the snapshot (max 1000 characters) + + metadata: (Optional) Metadata used to describe the snapshot + + name: (Optional) A user specified name to give the snapshot + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/devboxes/disk_snapshots/{id}", + body=maybe_transform( + { + "commit_message": commit_message, + "metadata": metadata, + "name": name, + }, + disk_snapshot_update_params.DiskSnapshotUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxSnapshotView, + ) + + def list( + self, + *, + devbox_id: str | Omit = omit, + limit: int | Omit = omit, + metadata_key: str | Omit = omit, + metadata_key_in: str | Omit = omit, + source_blueprint_id: str | Omit = omit, + starting_after: 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, + ) -> SyncDiskSnapshotsCursorIDPage[DevboxSnapshotView]: + """ + List all snapshots of a Devbox while optionally filtering by Devbox ID, source + Blueprint ID, and metadata. + + Args: + devbox_id: Devbox ID to filter by. + + limit: The limit of items to return. Default is 20. Max is 5000. + + metadata_key: Filter snapshots by metadata key-value pair. Can be used multiple times for + different keys. + + metadata_key_in: Filter snapshots by metadata key with multiple possible values (OR condition). + + source_blueprint_id: Source Blueprint ID to filter snapshots by. + + starting_after: Load the next page of data starting after the item with the given ID. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/devboxes/disk_snapshots", + page=SyncDiskSnapshotsCursorIDPage[DevboxSnapshotView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "devbox_id": devbox_id, + "limit": limit, + "metadata_key": metadata_key, + "metadata_key_in": metadata_key_in, + "source_blueprint_id": source_blueprint_id, + "starting_after": starting_after, + }, + disk_snapshot_list_params.DiskSnapshotListParams, + ), + ), + model=DevboxSnapshotView, + ) + + 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, + idempotency_key: str | None = None, + ) -> object: + """ + Delete a previously taken disk snapshot of a Devbox. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/devboxes/disk_snapshots/{id}/delete", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=object, + ) + + def query_status( + 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, + ) -> DevboxSnapshotAsyncStatusView: + """ + Get the current status of an asynchronous disk snapshot operation, including + whether it is still in progress and any error messages if it failed. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/v1/devboxes/disk_snapshots/{id}/status", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=DevboxSnapshotAsyncStatusView, + ) + + +class AsyncDiskSnapshotsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncDiskSnapshotsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return AsyncDiskSnapshotsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncDiskSnapshotsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return AsyncDiskSnapshotsResourceWithStreamingResponse(self) + + async def update( + self, + id: str, + *, + commit_message: Optional[str] | Omit = omit, + metadata: Optional[Dict[str, str]] | Omit = omit, + name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> DevboxSnapshotView: + """Updates disk snapshot metadata via update vs patch. + + The entire metadata will be + replaced. + + Args: + commit_message: (Optional) Commit message associated with the snapshot (max 1000 characters) + + metadata: (Optional) Metadata used to describe the snapshot + + name: (Optional) A user specified name to give the snapshot + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/devboxes/disk_snapshots/{id}", + body=await async_maybe_transform( + { + "commit_message": commit_message, + "metadata": metadata, + "name": name, + }, + disk_snapshot_update_params.DiskSnapshotUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxSnapshotView, + ) + + def list( + self, + *, + devbox_id: str | Omit = omit, + limit: int | Omit = omit, + metadata_key: str | Omit = omit, + metadata_key_in: str | Omit = omit, + source_blueprint_id: str | Omit = omit, + starting_after: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[DevboxSnapshotView, AsyncDiskSnapshotsCursorIDPage[DevboxSnapshotView]]: + """ + List all snapshots of a Devbox while optionally filtering by Devbox ID, source + Blueprint ID, and metadata. + + Args: + devbox_id: Devbox ID to filter by. + + limit: The limit of items to return. Default is 20. Max is 5000. + + metadata_key: Filter snapshots by metadata key-value pair. Can be used multiple times for + different keys. + + metadata_key_in: Filter snapshots by metadata key with multiple possible values (OR condition). + + source_blueprint_id: Source Blueprint ID to filter snapshots by. + + starting_after: Load the next page of data starting after the item with the given ID. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/devboxes/disk_snapshots", + page=AsyncDiskSnapshotsCursorIDPage[DevboxSnapshotView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "devbox_id": devbox_id, + "limit": limit, + "metadata_key": metadata_key, + "metadata_key_in": metadata_key_in, + "source_blueprint_id": source_blueprint_id, + "starting_after": starting_after, + }, + disk_snapshot_list_params.DiskSnapshotListParams, + ), + ), + model=DevboxSnapshotView, + ) + + 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, + idempotency_key: str | None = None, + ) -> object: + """ + Delete a previously taken disk snapshot of a Devbox. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/devboxes/disk_snapshots/{id}/delete", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=object, + ) + + async def query_status( + 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, + ) -> DevboxSnapshotAsyncStatusView: + """ + Get the current status of an asynchronous disk snapshot operation, including + whether it is still in progress and any error messages if it failed. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/v1/devboxes/disk_snapshots/{id}/status", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=DevboxSnapshotAsyncStatusView, + ) + + +class DiskSnapshotsResourceWithRawResponse: + def __init__(self, disk_snapshots: DiskSnapshotsResource) -> None: + self._disk_snapshots = disk_snapshots + + self.update = to_raw_response_wrapper( + disk_snapshots.update, + ) + self.list = to_raw_response_wrapper( + disk_snapshots.list, + ) + self.delete = to_raw_response_wrapper( + disk_snapshots.delete, + ) + self.query_status = to_raw_response_wrapper( + disk_snapshots.query_status, + ) + + +class AsyncDiskSnapshotsResourceWithRawResponse: + def __init__(self, disk_snapshots: AsyncDiskSnapshotsResource) -> None: + self._disk_snapshots = disk_snapshots + + self.update = async_to_raw_response_wrapper( + disk_snapshots.update, + ) + self.list = async_to_raw_response_wrapper( + disk_snapshots.list, + ) + self.delete = async_to_raw_response_wrapper( + disk_snapshots.delete, + ) + self.query_status = async_to_raw_response_wrapper( + disk_snapshots.query_status, + ) + + +class DiskSnapshotsResourceWithStreamingResponse: + def __init__(self, disk_snapshots: DiskSnapshotsResource) -> None: + self._disk_snapshots = disk_snapshots + + self.update = to_streamed_response_wrapper( + disk_snapshots.update, + ) + self.list = to_streamed_response_wrapper( + disk_snapshots.list, + ) + self.delete = to_streamed_response_wrapper( + disk_snapshots.delete, + ) + self.query_status = to_streamed_response_wrapper( + disk_snapshots.query_status, + ) + + +class AsyncDiskSnapshotsResourceWithStreamingResponse: + def __init__(self, disk_snapshots: AsyncDiskSnapshotsResource) -> None: + self._disk_snapshots = disk_snapshots + + self.update = async_to_streamed_response_wrapper( + disk_snapshots.update, + ) + self.list = async_to_streamed_response_wrapper( + disk_snapshots.list, + ) + self.delete = async_to_streamed_response_wrapper( + disk_snapshots.delete, + ) + self.query_status = async_to_streamed_response_wrapper( + disk_snapshots.query_status, + ) diff --git a/src/runloop_api_client/resources/devboxes/executions.py b/src/runloop_api_client/resources/devboxes/executions.py new file mode 100755 index 000000000..aa85e339c --- /dev/null +++ b/src/runloop_api_client/resources/devboxes/executions.py @@ -0,0 +1,954 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import typing_extensions +from typing import Optional +from typing_extensions import Literal + +import httpx + +from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from ..._utils import is_given, 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 ..._constants import DEFAULT_TIMEOUT +from ..._streaming import Stream, AsyncStream +from ..._base_client import make_request_options +from ...types.devboxes import ( + execution_kill_params, + execution_retrieve_params, + execution_send_std_in_params, + execution_execute_sync_params, + execution_execute_async_params, + execution_stream_stderr_updates_params, + execution_stream_stdout_updates_params, +) +from ...types.devbox_send_std_in_result import DevboxSendStdInResult +from ...types.devbox_execution_detail_view import DevboxExecutionDetailView +from ...types.devboxes.execution_update_chunk import ExecutionUpdateChunk +from ...types.devbox_async_execution_detail_view import DevboxAsyncExecutionDetailView + +__all__ = ["ExecutionsResource", "AsyncExecutionsResource"] + + +class ExecutionsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> ExecutionsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return ExecutionsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ExecutionsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return ExecutionsResourceWithStreamingResponse(self) + + def retrieve( + self, + execution_id: str, + *, + devbox_id: str, + last_n: 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, + ) -> DevboxAsyncExecutionDetailView: + """ + Get the latest status of a previously launched asynchronous execuction including + stdout/error and the exit code if complete. + + Args: + last_n: Last n lines of standard error / standard out to return (default: 100) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not devbox_id: + raise ValueError(f"Expected a non-empty value for `devbox_id` but received {devbox_id!r}") + if not execution_id: + raise ValueError(f"Expected a non-empty value for `execution_id` but received {execution_id!r}") + return self._get( + f"/v1/devboxes/{devbox_id}/executions/{execution_id}", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform({"last_n": last_n}, execution_retrieve_params.ExecutionRetrieveParams), + ), + cast_to=DevboxAsyncExecutionDetailView, + ) + + def execute_async( + self, + id: str, + *, + command: str, + attach_stdin: Optional[bool] | Omit = omit, + shell_name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> DevboxAsyncExecutionDetailView: + """ + Execute the given command in the Devbox shell asynchronously and returns the + execution that can be used to track the command's progress. + + Args: + command: The command to execute via the Devbox shell. By default, commands are run from + the user home directory unless shell_name is specified. If shell_name is + specified the command is run from the directory based on the recent state of the + persistent shell. + + attach_stdin: Whether to attach stdin streaming for async commands. Not valid for execute_sync + endpoint. Defaults to false if not specified. + + shell_name: The name of the persistent shell to create or use if already created. When using + a persistent shell, the command will run from the directory at the end of the + previous command and environment variables will be preserved. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/devboxes/{id}/execute_async", + body=maybe_transform( + { + "command": command, + "attach_stdin": attach_stdin, + "shell_name": shell_name, + }, + execution_execute_async_params.ExecutionExecuteAsyncParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxAsyncExecutionDetailView, + ) + + @typing_extensions.deprecated("deprecated") + def execute_sync( + self, + id: str, + *, + command: str, + attach_stdin: Optional[bool] | Omit = omit, + shell_name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> DevboxExecutionDetailView: + """ + Execute a bash command in the Devbox shell, await the command completion and + return the output. Note: attach_stdin parameter is not supported for synchronous + execution. + + Args: + command: The command to execute via the Devbox shell. By default, commands are run from + the user home directory unless shell_name is specified. If shell_name is + specified the command is run from the directory based on the recent state of the + persistent shell. + + attach_stdin: Whether to attach stdin streaming for async commands. Not valid for execute_sync + endpoint. Defaults to false if not specified. + + shell_name: The name of the persistent shell to create or use if already created. When using + a persistent shell, the command will run from the directory at the end of the + previous command and environment variables will be preserved. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: + timeout = 600 + return self._post( + f"/v1/devboxes/{id}/execute_sync", + body=maybe_transform( + { + "command": command, + "attach_stdin": attach_stdin, + "shell_name": shell_name, + }, + execution_execute_sync_params.ExecutionExecuteSyncParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxExecutionDetailView, + ) + + def kill( + self, + execution_id: str, + *, + devbox_id: str, + kill_process_group: Optional[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, + idempotency_key: str | None = None, + ) -> DevboxAsyncExecutionDetailView: + """ + Kill a previously launched asynchronous execution if it is still running by + killing the launched process. Optionally kill the entire process group. + + Args: + kill_process_group: Whether to kill the entire process group (default: false). If true, kills all + processes in the same process group as the target process. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not devbox_id: + raise ValueError(f"Expected a non-empty value for `devbox_id` but received {devbox_id!r}") + if not execution_id: + raise ValueError(f"Expected a non-empty value for `execution_id` but received {execution_id!r}") + return self._post( + f"/v1/devboxes/{devbox_id}/executions/{execution_id}/kill", + body=maybe_transform({"kill_process_group": kill_process_group}, execution_kill_params.ExecutionKillParams), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxAsyncExecutionDetailView, + ) + + def send_std_in( + self, + execution_id: str, + *, + devbox_id: str, + signal: Optional[Literal["EOF", "INTERRUPT"]] | Omit = omit, + text: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> DevboxSendStdInResult: + """ + Send content to the Std In of a running execution. + + Args: + signal: Signal to send to std in of the running execution. + + text: Text to send to std in of the running execution. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not devbox_id: + raise ValueError(f"Expected a non-empty value for `devbox_id` but received {devbox_id!r}") + if not execution_id: + raise ValueError(f"Expected a non-empty value for `execution_id` but received {execution_id!r}") + return self._post( + f"/v1/devboxes/{devbox_id}/executions/{execution_id}/send_std_in", + body=maybe_transform( + { + "signal": signal, + "text": text, + }, + execution_send_std_in_params.ExecutionSendStdInParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxSendStdInResult, + ) + + def stream_stderr_updates( + self, + execution_id: str, + *, + devbox_id: str, + offset: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Stream[ExecutionUpdateChunk]: + """ + Tails the stderr logs for the given execution with SSE streaming + + Args: + offset: The byte offset to start the stream from (if unspecified, starts from the + beginning of the stream) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not devbox_id: + raise ValueError(f"Expected a non-empty value for `devbox_id` but received {devbox_id!r}") + if not execution_id: + raise ValueError(f"Expected a non-empty value for `execution_id` but received {execution_id!r}") + return self._get( + f"/v1/devboxes/{devbox_id}/executions/{execution_id}/stream_stderr_updates", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + {"offset": offset}, execution_stream_stderr_updates_params.ExecutionStreamStderrUpdatesParams + ), + ), + cast_to=ExecutionUpdateChunk, + stream=True, + stream_cls=Stream[ExecutionUpdateChunk], + ) + + def stream_stdout_updates( + self, + execution_id: str, + *, + devbox_id: str, + offset: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Stream[ExecutionUpdateChunk]: + """ + Tails the stdout logs for the given execution with SSE streaming + + Args: + offset: The byte offset to start the stream from (if unspecified, starts from the + beginning of the stream) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not devbox_id: + raise ValueError(f"Expected a non-empty value for `devbox_id` but received {devbox_id!r}") + if not execution_id: + raise ValueError(f"Expected a non-empty value for `execution_id` but received {execution_id!r}") + return self._get( + f"/v1/devboxes/{devbox_id}/executions/{execution_id}/stream_stdout_updates", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + {"offset": offset}, execution_stream_stdout_updates_params.ExecutionStreamStdoutUpdatesParams + ), + ), + cast_to=ExecutionUpdateChunk, + stream=True, + stream_cls=Stream[ExecutionUpdateChunk], + ) + + +class AsyncExecutionsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncExecutionsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return AsyncExecutionsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncExecutionsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return AsyncExecutionsResourceWithStreamingResponse(self) + + async def retrieve( + self, + execution_id: str, + *, + devbox_id: str, + last_n: 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, + ) -> DevboxAsyncExecutionDetailView: + """ + Get the latest status of a previously launched asynchronous execuction including + stdout/error and the exit code if complete. + + Args: + last_n: Last n lines of standard error / standard out to return (default: 100) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not devbox_id: + raise ValueError(f"Expected a non-empty value for `devbox_id` but received {devbox_id!r}") + if not execution_id: + raise ValueError(f"Expected a non-empty value for `execution_id` but received {execution_id!r}") + return await self._get( + f"/v1/devboxes/{devbox_id}/executions/{execution_id}", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + {"last_n": last_n}, execution_retrieve_params.ExecutionRetrieveParams + ), + ), + cast_to=DevboxAsyncExecutionDetailView, + ) + + async def execute_async( + self, + id: str, + *, + command: str, + attach_stdin: Optional[bool] | Omit = omit, + shell_name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> DevboxAsyncExecutionDetailView: + """ + Execute the given command in the Devbox shell asynchronously and returns the + execution that can be used to track the command's progress. + + Args: + command: The command to execute via the Devbox shell. By default, commands are run from + the user home directory unless shell_name is specified. If shell_name is + specified the command is run from the directory based on the recent state of the + persistent shell. + + attach_stdin: Whether to attach stdin streaming for async commands. Not valid for execute_sync + endpoint. Defaults to false if not specified. + + shell_name: The name of the persistent shell to create or use if already created. When using + a persistent shell, the command will run from the directory at the end of the + previous command and environment variables will be preserved. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/devboxes/{id}/execute_async", + body=await async_maybe_transform( + { + "command": command, + "attach_stdin": attach_stdin, + "shell_name": shell_name, + }, + execution_execute_async_params.ExecutionExecuteAsyncParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxAsyncExecutionDetailView, + ) + + @typing_extensions.deprecated("deprecated") + async def execute_sync( + self, + id: str, + *, + command: str, + attach_stdin: Optional[bool] | Omit = omit, + shell_name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> DevboxExecutionDetailView: + """ + Execute a bash command in the Devbox shell, await the command completion and + return the output. Note: attach_stdin parameter is not supported for synchronous + execution. + + Args: + command: The command to execute via the Devbox shell. By default, commands are run from + the user home directory unless shell_name is specified. If shell_name is + specified the command is run from the directory based on the recent state of the + persistent shell. + + attach_stdin: Whether to attach stdin streaming for async commands. Not valid for execute_sync + endpoint. Defaults to false if not specified. + + shell_name: The name of the persistent shell to create or use if already created. When using + a persistent shell, the command will run from the directory at the end of the + previous command and environment variables will be preserved. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: + timeout = 600 + return await self._post( + f"/v1/devboxes/{id}/execute_sync", + body=await async_maybe_transform( + { + "command": command, + "attach_stdin": attach_stdin, + "shell_name": shell_name, + }, + execution_execute_sync_params.ExecutionExecuteSyncParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxExecutionDetailView, + ) + + async def kill( + self, + execution_id: str, + *, + devbox_id: str, + kill_process_group: Optional[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, + idempotency_key: str | None = None, + ) -> DevboxAsyncExecutionDetailView: + """ + Kill a previously launched asynchronous execution if it is still running by + killing the launched process. Optionally kill the entire process group. + + Args: + kill_process_group: Whether to kill the entire process group (default: false). If true, kills all + processes in the same process group as the target process. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not devbox_id: + raise ValueError(f"Expected a non-empty value for `devbox_id` but received {devbox_id!r}") + if not execution_id: + raise ValueError(f"Expected a non-empty value for `execution_id` but received {execution_id!r}") + return await self._post( + f"/v1/devboxes/{devbox_id}/executions/{execution_id}/kill", + body=await async_maybe_transform( + {"kill_process_group": kill_process_group}, execution_kill_params.ExecutionKillParams + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxAsyncExecutionDetailView, + ) + + async def send_std_in( + self, + execution_id: str, + *, + devbox_id: str, + signal: Optional[Literal["EOF", "INTERRUPT"]] | Omit = omit, + text: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> DevboxSendStdInResult: + """ + Send content to the Std In of a running execution. + + Args: + signal: Signal to send to std in of the running execution. + + text: Text to send to std in of the running execution. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not devbox_id: + raise ValueError(f"Expected a non-empty value for `devbox_id` but received {devbox_id!r}") + if not execution_id: + raise ValueError(f"Expected a non-empty value for `execution_id` but received {execution_id!r}") + return await self._post( + f"/v1/devboxes/{devbox_id}/executions/{execution_id}/send_std_in", + body=await async_maybe_transform( + { + "signal": signal, + "text": text, + }, + execution_send_std_in_params.ExecutionSendStdInParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxSendStdInResult, + ) + + async def stream_stderr_updates( + self, + execution_id: str, + *, + devbox_id: str, + offset: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncStream[ExecutionUpdateChunk]: + """ + Tails the stderr logs for the given execution with SSE streaming + + Args: + offset: The byte offset to start the stream from (if unspecified, starts from the + beginning of the stream) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not devbox_id: + raise ValueError(f"Expected a non-empty value for `devbox_id` but received {devbox_id!r}") + if not execution_id: + raise ValueError(f"Expected a non-empty value for `execution_id` but received {execution_id!r}") + return await self._get( + f"/v1/devboxes/{devbox_id}/executions/{execution_id}/stream_stderr_updates", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + {"offset": offset}, execution_stream_stderr_updates_params.ExecutionStreamStderrUpdatesParams + ), + ), + cast_to=ExecutionUpdateChunk, + stream=True, + stream_cls=AsyncStream[ExecutionUpdateChunk], + ) + + async def stream_stdout_updates( + self, + execution_id: str, + *, + devbox_id: str, + offset: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncStream[ExecutionUpdateChunk]: + """ + Tails the stdout logs for the given execution with SSE streaming + + Args: + offset: The byte offset to start the stream from (if unspecified, starts from the + beginning of the stream) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not devbox_id: + raise ValueError(f"Expected a non-empty value for `devbox_id` but received {devbox_id!r}") + if not execution_id: + raise ValueError(f"Expected a non-empty value for `execution_id` but received {execution_id!r}") + return await self._get( + f"/v1/devboxes/{devbox_id}/executions/{execution_id}/stream_stdout_updates", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + {"offset": offset}, execution_stream_stdout_updates_params.ExecutionStreamStdoutUpdatesParams + ), + ), + cast_to=ExecutionUpdateChunk, + stream=True, + stream_cls=AsyncStream[ExecutionUpdateChunk], + ) + + +class ExecutionsResourceWithRawResponse: + def __init__(self, executions: ExecutionsResource) -> None: + self._executions = executions + + self.retrieve = to_raw_response_wrapper( + executions.retrieve, + ) + self.execute_async = to_raw_response_wrapper( + executions.execute_async, + ) + self.execute_sync = ( # pyright: ignore[reportDeprecated] + to_raw_response_wrapper( + executions.execute_sync, # pyright: ignore[reportDeprecated], + ) + ) + self.kill = to_raw_response_wrapper( + executions.kill, + ) + self.send_std_in = to_raw_response_wrapper( + executions.send_std_in, + ) + self.stream_stderr_updates = to_raw_response_wrapper( + executions.stream_stderr_updates, + ) + self.stream_stdout_updates = to_raw_response_wrapper( + executions.stream_stdout_updates, + ) + + +class AsyncExecutionsResourceWithRawResponse: + def __init__(self, executions: AsyncExecutionsResource) -> None: + self._executions = executions + + self.retrieve = async_to_raw_response_wrapper( + executions.retrieve, + ) + self.execute_async = async_to_raw_response_wrapper( + executions.execute_async, + ) + self.execute_sync = ( # pyright: ignore[reportDeprecated] + async_to_raw_response_wrapper( + executions.execute_sync, # pyright: ignore[reportDeprecated], + ) + ) + self.kill = async_to_raw_response_wrapper( + executions.kill, + ) + self.send_std_in = async_to_raw_response_wrapper( + executions.send_std_in, + ) + self.stream_stderr_updates = async_to_raw_response_wrapper( + executions.stream_stderr_updates, + ) + self.stream_stdout_updates = async_to_raw_response_wrapper( + executions.stream_stdout_updates, + ) + + +class ExecutionsResourceWithStreamingResponse: + def __init__(self, executions: ExecutionsResource) -> None: + self._executions = executions + + self.retrieve = to_streamed_response_wrapper( + executions.retrieve, + ) + self.execute_async = to_streamed_response_wrapper( + executions.execute_async, + ) + self.execute_sync = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + executions.execute_sync, # pyright: ignore[reportDeprecated], + ) + ) + self.kill = to_streamed_response_wrapper( + executions.kill, + ) + self.send_std_in = to_streamed_response_wrapper( + executions.send_std_in, + ) + self.stream_stderr_updates = to_streamed_response_wrapper( + executions.stream_stderr_updates, + ) + self.stream_stdout_updates = to_streamed_response_wrapper( + executions.stream_stdout_updates, + ) + + +class AsyncExecutionsResourceWithStreamingResponse: + def __init__(self, executions: AsyncExecutionsResource) -> None: + self._executions = executions + + self.retrieve = async_to_streamed_response_wrapper( + executions.retrieve, + ) + self.execute_async = async_to_streamed_response_wrapper( + executions.execute_async, + ) + self.execute_sync = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + executions.execute_sync, # pyright: ignore[reportDeprecated], + ) + ) + self.kill = async_to_streamed_response_wrapper( + executions.kill, + ) + self.send_std_in = async_to_streamed_response_wrapper( + executions.send_std_in, + ) + self.stream_stderr_updates = async_to_streamed_response_wrapper( + executions.stream_stderr_updates, + ) + self.stream_stdout_updates = async_to_streamed_response_wrapper( + executions.stream_stdout_updates, + ) diff --git a/src/runloop_api_client/resources/devboxes/logs.py b/src/runloop_api_client/resources/devboxes/logs.py new file mode 100644 index 000000000..19d2c06e1 --- /dev/null +++ b/src/runloop_api_client/resources/devboxes/logs.py @@ -0,0 +1,197 @@ +# 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.devboxes import log_list_params +from ...types.devboxes.devbox_logs_list_view import DevboxLogsListView + +__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/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return LogsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> LogsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return LogsResourceWithStreamingResponse(self) + + def list( + self, + id: str, + *, + execution_id: str | Omit = omit, + shell_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, + ) -> DevboxLogsListView: + """ + Get all logs from a running or completed Devbox. + + Args: + execution_id: ID of execution to filter logs by. + + shell_name: Shell Name to filter logs by. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/v1/devboxes/{id}/logs", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "execution_id": execution_id, + "shell_name": shell_name, + }, + log_list_params.LogListParams, + ), + ), + cast_to=DevboxLogsListView, + ) + + +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/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return AsyncLogsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncLogsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return AsyncLogsResourceWithStreamingResponse(self) + + async def list( + self, + id: str, + *, + execution_id: str | Omit = omit, + shell_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, + ) -> DevboxLogsListView: + """ + Get all logs from a running or completed Devbox. + + Args: + execution_id: ID of execution to filter logs by. + + shell_name: Shell Name to filter logs by. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/v1/devboxes/{id}/logs", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "execution_id": execution_id, + "shell_name": shell_name, + }, + log_list_params.LogListParams, + ), + ), + cast_to=DevboxLogsListView, + ) + + +class LogsResourceWithRawResponse: + def __init__(self, logs: LogsResource) -> None: + self._logs = logs + + self.list = to_raw_response_wrapper( + logs.list, + ) + + +class AsyncLogsResourceWithRawResponse: + def __init__(self, logs: AsyncLogsResource) -> None: + self._logs = logs + + self.list = async_to_raw_response_wrapper( + logs.list, + ) + + +class LogsResourceWithStreamingResponse: + def __init__(self, logs: LogsResource) -> None: + self._logs = logs + + self.list = to_streamed_response_wrapper( + logs.list, + ) + + +class AsyncLogsResourceWithStreamingResponse: + def __init__(self, logs: AsyncLogsResource) -> None: + self._logs = logs + + self.list = async_to_streamed_response_wrapper( + logs.list, + ) diff --git a/src/runloop_api_client/resources/gateway_configs.py b/src/runloop_api_client/resources/gateway_configs.py new file mode 100644 index 000000000..86ec22ce6 --- /dev/null +++ b/src/runloop_api_client/resources/gateway_configs.py @@ -0,0 +1,658 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional + +import httpx + +from ..types import gateway_config_list_params, gateway_config_create_params, gateway_config_update_params +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..pagination import SyncGatewayConfigsCursorIDPage, AsyncGatewayConfigsCursorIDPage +from .._base_client import AsyncPaginator, make_request_options +from ..types.gateway_config_view import GatewayConfigView + +__all__ = ["GatewayConfigsResource", "AsyncGatewayConfigsResource"] + + +class GatewayConfigsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> GatewayConfigsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return GatewayConfigsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> GatewayConfigsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return GatewayConfigsResourceWithStreamingResponse(self) + + def create( + self, + *, + auth_mechanism: gateway_config_create_params.AuthMechanism, + endpoint: str, + name: str, + description: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> GatewayConfigView: + """Create a new GatewayConfig to proxy API requests through the agent gateway. + + The + config specifies the target endpoint and how credentials should be applied. + + Args: + auth_mechanism: How credentials should be applied to proxied requests. Specify the type + ('header', 'bearer') and optional key field. + + endpoint: The target endpoint URL (e.g., 'https://api.anthropic.com'). + + name: The human-readable name for the GatewayConfig. Must be unique within your + account. + + description: Optional description for this gateway 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 + + idempotency_key: Specify a custom idempotency key for this request + """ + return self._post( + "/v1/gateway-configs", + body=maybe_transform( + { + "auth_mechanism": auth_mechanism, + "endpoint": endpoint, + "name": name, + "description": description, + }, + gateway_config_create_params.GatewayConfigCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=GatewayConfigView, + ) + + 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, + ) -> GatewayConfigView: + """ + Get a specific GatewayConfig by its unique identifier. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/v1/gateway-configs/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GatewayConfigView, + ) + + def update( + self, + id: str, + *, + auth_mechanism: Optional[gateway_config_update_params.AuthMechanism] | Omit = omit, + description: Optional[str] | Omit = omit, + endpoint: Optional[str] | Omit = omit, + name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> GatewayConfigView: + """Update an existing GatewayConfig. + + All fields are optional. + + Args: + auth_mechanism: New authentication mechanism for applying credentials to proxied requests. + + description: New description for this gateway configuration. + + endpoint: New target endpoint URL (e.g., 'https://api.anthropic.com'). + + name: New name for the GatewayConfig. Must be unique within your account. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/gateway-configs/{id}", + body=maybe_transform( + { + "auth_mechanism": auth_mechanism, + "description": description, + "endpoint": endpoint, + "name": name, + }, + gateway_config_update_params.GatewayConfigUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=GatewayConfigView, + ) + + def list( + self, + *, + id: str | Omit = omit, + limit: int | Omit = omit, + name: str | Omit = omit, + starting_after: 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, + ) -> SyncGatewayConfigsCursorIDPage[GatewayConfigView]: + """ + List all GatewayConfigs for the authenticated account, including system-provided + configs like 'anthropic' and 'openai'. + + Args: + id: Filter by ID. + + limit: The limit of items to return. Default is 20. Max is 5000. + + name: Filter by name (partial match supported). + + starting_after: Load the next page of data starting after the item with the given ID. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/gateway-configs", + page=SyncGatewayConfigsCursorIDPage[GatewayConfigView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "id": id, + "limit": limit, + "name": name, + "starting_after": starting_after, + }, + gateway_config_list_params.GatewayConfigListParams, + ), + ), + model=GatewayConfigView, + ) + + 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, + idempotency_key: str | None = None, + ) -> GatewayConfigView: + """Delete an existing GatewayConfig. + + This action is irreversible. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/gateway-configs/{id}/delete", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=GatewayConfigView, + ) + + +class AsyncGatewayConfigsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncGatewayConfigsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return AsyncGatewayConfigsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncGatewayConfigsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return AsyncGatewayConfigsResourceWithStreamingResponse(self) + + async def create( + self, + *, + auth_mechanism: gateway_config_create_params.AuthMechanism, + endpoint: str, + name: str, + description: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> GatewayConfigView: + """Create a new GatewayConfig to proxy API requests through the agent gateway. + + The + config specifies the target endpoint and how credentials should be applied. + + Args: + auth_mechanism: How credentials should be applied to proxied requests. Specify the type + ('header', 'bearer') and optional key field. + + endpoint: The target endpoint URL (e.g., 'https://api.anthropic.com'). + + name: The human-readable name for the GatewayConfig. Must be unique within your + account. + + description: Optional description for this gateway 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 + + idempotency_key: Specify a custom idempotency key for this request + """ + return await self._post( + "/v1/gateway-configs", + body=await async_maybe_transform( + { + "auth_mechanism": auth_mechanism, + "endpoint": endpoint, + "name": name, + "description": description, + }, + gateway_config_create_params.GatewayConfigCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=GatewayConfigView, + ) + + 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, + ) -> GatewayConfigView: + """ + Get a specific GatewayConfig by its unique identifier. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/v1/gateway-configs/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GatewayConfigView, + ) + + async def update( + self, + id: str, + *, + auth_mechanism: Optional[gateway_config_update_params.AuthMechanism] | Omit = omit, + description: Optional[str] | Omit = omit, + endpoint: Optional[str] | Omit = omit, + name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> GatewayConfigView: + """Update an existing GatewayConfig. + + All fields are optional. + + Args: + auth_mechanism: New authentication mechanism for applying credentials to proxied requests. + + description: New description for this gateway configuration. + + endpoint: New target endpoint URL (e.g., 'https://api.anthropic.com'). + + name: New name for the GatewayConfig. Must be unique within your account. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/gateway-configs/{id}", + body=await async_maybe_transform( + { + "auth_mechanism": auth_mechanism, + "description": description, + "endpoint": endpoint, + "name": name, + }, + gateway_config_update_params.GatewayConfigUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=GatewayConfigView, + ) + + def list( + self, + *, + id: str | Omit = omit, + limit: int | Omit = omit, + name: str | Omit = omit, + starting_after: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[GatewayConfigView, AsyncGatewayConfigsCursorIDPage[GatewayConfigView]]: + """ + List all GatewayConfigs for the authenticated account, including system-provided + configs like 'anthropic' and 'openai'. + + Args: + id: Filter by ID. + + limit: The limit of items to return. Default is 20. Max is 5000. + + name: Filter by name (partial match supported). + + starting_after: Load the next page of data starting after the item with the given ID. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/gateway-configs", + page=AsyncGatewayConfigsCursorIDPage[GatewayConfigView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "id": id, + "limit": limit, + "name": name, + "starting_after": starting_after, + }, + gateway_config_list_params.GatewayConfigListParams, + ), + ), + model=GatewayConfigView, + ) + + 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, + idempotency_key: str | None = None, + ) -> GatewayConfigView: + """Delete an existing GatewayConfig. + + This action is irreversible. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/gateway-configs/{id}/delete", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=GatewayConfigView, + ) + + +class GatewayConfigsResourceWithRawResponse: + def __init__(self, gateway_configs: GatewayConfigsResource) -> None: + self._gateway_configs = gateway_configs + + self.create = to_raw_response_wrapper( + gateway_configs.create, + ) + self.retrieve = to_raw_response_wrapper( + gateway_configs.retrieve, + ) + self.update = to_raw_response_wrapper( + gateway_configs.update, + ) + self.list = to_raw_response_wrapper( + gateway_configs.list, + ) + self.delete = to_raw_response_wrapper( + gateway_configs.delete, + ) + + +class AsyncGatewayConfigsResourceWithRawResponse: + def __init__(self, gateway_configs: AsyncGatewayConfigsResource) -> None: + self._gateway_configs = gateway_configs + + self.create = async_to_raw_response_wrapper( + gateway_configs.create, + ) + self.retrieve = async_to_raw_response_wrapper( + gateway_configs.retrieve, + ) + self.update = async_to_raw_response_wrapper( + gateway_configs.update, + ) + self.list = async_to_raw_response_wrapper( + gateway_configs.list, + ) + self.delete = async_to_raw_response_wrapper( + gateway_configs.delete, + ) + + +class GatewayConfigsResourceWithStreamingResponse: + def __init__(self, gateway_configs: GatewayConfigsResource) -> None: + self._gateway_configs = gateway_configs + + self.create = to_streamed_response_wrapper( + gateway_configs.create, + ) + self.retrieve = to_streamed_response_wrapper( + gateway_configs.retrieve, + ) + self.update = to_streamed_response_wrapper( + gateway_configs.update, + ) + self.list = to_streamed_response_wrapper( + gateway_configs.list, + ) + self.delete = to_streamed_response_wrapper( + gateway_configs.delete, + ) + + +class AsyncGatewayConfigsResourceWithStreamingResponse: + def __init__(self, gateway_configs: AsyncGatewayConfigsResource) -> None: + self._gateway_configs = gateway_configs + + self.create = async_to_streamed_response_wrapper( + gateway_configs.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + gateway_configs.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + gateway_configs.update, + ) + self.list = async_to_streamed_response_wrapper( + gateway_configs.list, + ) + self.delete = async_to_streamed_response_wrapper( + gateway_configs.delete, + ) diff --git a/src/runloop_api_client/resources/mcp_configs.py b/src/runloop_api_client/resources/mcp_configs.py new file mode 100644 index 000000000..24b05cc2d --- /dev/null +++ b/src/runloop_api_client/resources/mcp_configs.py @@ -0,0 +1,662 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional + +import httpx + +from ..types import mcp_config_list_params, mcp_config_create_params, mcp_config_update_params +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 +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..pagination import SyncMcpConfigsCursorIDPage, AsyncMcpConfigsCursorIDPage +from .._base_client import AsyncPaginator, make_request_options +from ..types.mcp_config_view import McpConfigView + +__all__ = ["McpConfigsResource", "AsyncMcpConfigsResource"] + + +class McpConfigsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> McpConfigsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return McpConfigsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> McpConfigsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return McpConfigsResourceWithStreamingResponse(self) + + def create( + self, + *, + allowed_tools: SequenceNotStr[str], + endpoint: str, + name: str, + description: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> McpConfigView: + """ + [Beta] Create a new McpConfig to connect to an upstream MCP (Model Context + Protocol) server. The config specifies the target endpoint and which tools are + allowed. + + Args: + allowed_tools: + Glob patterns specifying which tools are allowed from this MCP server. Examples: + ['*'] for all tools, ['github.search_*', 'github.get_*'] for specific patterns. + + endpoint: The target MCP server endpoint URL (e.g., 'https://mcp.example.com'). + + name: The human-readable name for the McpConfig. Must be unique within your account. + The first segment before '-' is used as the service name for tool routing (e.g., + 'github-readonly' uses 'github' as the service name). + + description: Optional description for this MCP 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 + + idempotency_key: Specify a custom idempotency key for this request + """ + return self._post( + "/v1/mcp-configs", + body=maybe_transform( + { + "allowed_tools": allowed_tools, + "endpoint": endpoint, + "name": name, + "description": description, + }, + mcp_config_create_params.McpConfigCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=McpConfigView, + ) + + 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, + ) -> McpConfigView: + """ + [Beta] Get a specific McpConfig by its unique identifier. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/v1/mcp-configs/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=McpConfigView, + ) + + def update( + self, + id: str, + *, + allowed_tools: Optional[SequenceNotStr[str]] | Omit = omit, + description: Optional[str] | Omit = omit, + endpoint: Optional[str] | Omit = omit, + name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> McpConfigView: + """[Beta] Update an existing McpConfig. + + All fields are optional. + + Args: + allowed_tools: New glob patterns specifying which tools are allowed. Examples: ['*'] for all + tools, ['github.search_*'] for specific patterns. + + description: New description for this MCP configuration. + + endpoint: New target MCP server endpoint URL. + + name: New name for the McpConfig. Must be unique within your account. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/mcp-configs/{id}", + body=maybe_transform( + { + "allowed_tools": allowed_tools, + "description": description, + "endpoint": endpoint, + "name": name, + }, + mcp_config_update_params.McpConfigUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=McpConfigView, + ) + + def list( + self, + *, + id: str | Omit = omit, + limit: int | Omit = omit, + name: str | Omit = omit, + starting_after: 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, + ) -> SyncMcpConfigsCursorIDPage[McpConfigView]: + """ + [Beta] List all McpConfigs for the authenticated account. + + Args: + id: Filter by ID. + + limit: The limit of items to return. Default is 20. Max is 5000. + + name: Filter by name (prefix match supported). + + starting_after: Load the next page of data starting after the item with the given ID. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/mcp-configs", + page=SyncMcpConfigsCursorIDPage[McpConfigView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "id": id, + "limit": limit, + "name": name, + "starting_after": starting_after, + }, + mcp_config_list_params.McpConfigListParams, + ), + ), + model=McpConfigView, + ) + + 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, + idempotency_key: str | None = None, + ) -> McpConfigView: + """[Beta] Delete an existing McpConfig. + + This action is irreversible. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/mcp-configs/{id}/delete", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=McpConfigView, + ) + + +class AsyncMcpConfigsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncMcpConfigsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return AsyncMcpConfigsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncMcpConfigsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return AsyncMcpConfigsResourceWithStreamingResponse(self) + + async def create( + self, + *, + allowed_tools: SequenceNotStr[str], + endpoint: str, + name: str, + description: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> McpConfigView: + """ + [Beta] Create a new McpConfig to connect to an upstream MCP (Model Context + Protocol) server. The config specifies the target endpoint and which tools are + allowed. + + Args: + allowed_tools: + Glob patterns specifying which tools are allowed from this MCP server. Examples: + ['*'] for all tools, ['github.search_*', 'github.get_*'] for specific patterns. + + endpoint: The target MCP server endpoint URL (e.g., 'https://mcp.example.com'). + + name: The human-readable name for the McpConfig. Must be unique within your account. + The first segment before '-' is used as the service name for tool routing (e.g., + 'github-readonly' uses 'github' as the service name). + + description: Optional description for this MCP 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 + + idempotency_key: Specify a custom idempotency key for this request + """ + return await self._post( + "/v1/mcp-configs", + body=await async_maybe_transform( + { + "allowed_tools": allowed_tools, + "endpoint": endpoint, + "name": name, + "description": description, + }, + mcp_config_create_params.McpConfigCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=McpConfigView, + ) + + 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, + ) -> McpConfigView: + """ + [Beta] Get a specific McpConfig by its unique identifier. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/v1/mcp-configs/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=McpConfigView, + ) + + async def update( + self, + id: str, + *, + allowed_tools: Optional[SequenceNotStr[str]] | Omit = omit, + description: Optional[str] | Omit = omit, + endpoint: Optional[str] | Omit = omit, + name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> McpConfigView: + """[Beta] Update an existing McpConfig. + + All fields are optional. + + Args: + allowed_tools: New glob patterns specifying which tools are allowed. Examples: ['*'] for all + tools, ['github.search_*'] for specific patterns. + + description: New description for this MCP configuration. + + endpoint: New target MCP server endpoint URL. + + name: New name for the McpConfig. Must be unique within your account. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/mcp-configs/{id}", + body=await async_maybe_transform( + { + "allowed_tools": allowed_tools, + "description": description, + "endpoint": endpoint, + "name": name, + }, + mcp_config_update_params.McpConfigUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=McpConfigView, + ) + + def list( + self, + *, + id: str | Omit = omit, + limit: int | Omit = omit, + name: str | Omit = omit, + starting_after: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[McpConfigView, AsyncMcpConfigsCursorIDPage[McpConfigView]]: + """ + [Beta] List all McpConfigs for the authenticated account. + + Args: + id: Filter by ID. + + limit: The limit of items to return. Default is 20. Max is 5000. + + name: Filter by name (prefix match supported). + + starting_after: Load the next page of data starting after the item with the given ID. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/mcp-configs", + page=AsyncMcpConfigsCursorIDPage[McpConfigView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "id": id, + "limit": limit, + "name": name, + "starting_after": starting_after, + }, + mcp_config_list_params.McpConfigListParams, + ), + ), + model=McpConfigView, + ) + + 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, + idempotency_key: str | None = None, + ) -> McpConfigView: + """[Beta] Delete an existing McpConfig. + + This action is irreversible. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/mcp-configs/{id}/delete", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=McpConfigView, + ) + + +class McpConfigsResourceWithRawResponse: + def __init__(self, mcp_configs: McpConfigsResource) -> None: + self._mcp_configs = mcp_configs + + self.create = to_raw_response_wrapper( + mcp_configs.create, + ) + self.retrieve = to_raw_response_wrapper( + mcp_configs.retrieve, + ) + self.update = to_raw_response_wrapper( + mcp_configs.update, + ) + self.list = to_raw_response_wrapper( + mcp_configs.list, + ) + self.delete = to_raw_response_wrapper( + mcp_configs.delete, + ) + + +class AsyncMcpConfigsResourceWithRawResponse: + def __init__(self, mcp_configs: AsyncMcpConfigsResource) -> None: + self._mcp_configs = mcp_configs + + self.create = async_to_raw_response_wrapper( + mcp_configs.create, + ) + self.retrieve = async_to_raw_response_wrapper( + mcp_configs.retrieve, + ) + self.update = async_to_raw_response_wrapper( + mcp_configs.update, + ) + self.list = async_to_raw_response_wrapper( + mcp_configs.list, + ) + self.delete = async_to_raw_response_wrapper( + mcp_configs.delete, + ) + + +class McpConfigsResourceWithStreamingResponse: + def __init__(self, mcp_configs: McpConfigsResource) -> None: + self._mcp_configs = mcp_configs + + self.create = to_streamed_response_wrapper( + mcp_configs.create, + ) + self.retrieve = to_streamed_response_wrapper( + mcp_configs.retrieve, + ) + self.update = to_streamed_response_wrapper( + mcp_configs.update, + ) + self.list = to_streamed_response_wrapper( + mcp_configs.list, + ) + self.delete = to_streamed_response_wrapper( + mcp_configs.delete, + ) + + +class AsyncMcpConfigsResourceWithStreamingResponse: + def __init__(self, mcp_configs: AsyncMcpConfigsResource) -> None: + self._mcp_configs = mcp_configs + + self.create = async_to_streamed_response_wrapper( + mcp_configs.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + mcp_configs.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + mcp_configs.update, + ) + self.list = async_to_streamed_response_wrapper( + mcp_configs.list, + ) + self.delete = async_to_streamed_response_wrapper( + mcp_configs.delete, + ) diff --git a/src/runloop_api_client/resources/network_policies.py b/src/runloop_api_client/resources/network_policies.py new file mode 100644 index 000000000..f50d4fe6d --- /dev/null +++ b/src/runloop_api_client/resources/network_policies.py @@ -0,0 +1,716 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional + +import httpx + +from ..types import network_policy_list_params, network_policy_create_params, network_policy_update_params +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 +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..pagination import SyncNetworkPoliciesCursorIDPage, AsyncNetworkPoliciesCursorIDPage +from .._base_client import AsyncPaginator, make_request_options +from ..types.network_policy_view import NetworkPolicyView + +__all__ = ["NetworkPoliciesResource", "AsyncNetworkPoliciesResource"] + + +class NetworkPoliciesResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> NetworkPoliciesResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return NetworkPoliciesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> NetworkPoliciesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return NetworkPoliciesResourceWithStreamingResponse(self) + + def create( + self, + *, + name: str, + allow_agent_gateway: Optional[bool] | Omit = omit, + allow_all: Optional[bool] | Omit = omit, + allow_devbox_to_devbox: Optional[bool] | Omit = omit, + allow_mcp_gateway: Optional[bool] | Omit = omit, + allowed_hostnames: Optional[SequenceNotStr[str]] | Omit = omit, + description: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> NetworkPolicyView: + """Create a new NetworkPolicy with the specified egress rules. + + The policy can then + be applied to blueprints, devboxes, or snapshot resumes. + + Args: + name: The human-readable name for the NetworkPolicy. Must be unique within the + account. + + allow_agent_gateway: (Optional) If true, allows devbox egress to the agent gateway for credential + proxying. Defaults to false. + + allow_all: (Optional) If true, all egress traffic is allowed (ALLOW_ALL policy). Defaults + to false. + + allow_devbox_to_devbox: (Optional) If true, allows traffic between the account's own devboxes via + tunnels. Defaults to false. If allow_all is true, this is automatically set to + true. + + allow_mcp_gateway: (Optional) If true, allows devbox egress to the MCP hub for MCP server access. + Defaults to false. + + allowed_hostnames: (Optional) DNS-based allow list with wildcard support. Examples: ['github.com', + '*.npmjs.org']. + + description: Optional description for the NetworkPolicy. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + return self._post( + "/v1/network-policies", + body=maybe_transform( + { + "name": name, + "allow_agent_gateway": allow_agent_gateway, + "allow_all": allow_all, + "allow_devbox_to_devbox": allow_devbox_to_devbox, + "allow_mcp_gateway": allow_mcp_gateway, + "allowed_hostnames": allowed_hostnames, + "description": description, + }, + network_policy_create_params.NetworkPolicyCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=NetworkPolicyView, + ) + + 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, + ) -> NetworkPolicyView: + """ + Get a specific NetworkPolicy by its unique identifier. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/v1/network-policies/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NetworkPolicyView, + ) + + def update( + self, + id: str, + *, + allow_agent_gateway: Optional[bool] | Omit = omit, + allow_all: Optional[bool] | Omit = omit, + allow_devbox_to_devbox: Optional[bool] | Omit = omit, + allow_mcp_gateway: Optional[bool] | Omit = omit, + allowed_hostnames: Optional[SequenceNotStr[str]] | Omit = omit, + description: Optional[str] | Omit = omit, + name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> NetworkPolicyView: + """Update an existing NetworkPolicy. + + All fields are optional. + + Args: + allow_agent_gateway: If true, allows devbox egress to the agent gateway. + + allow_all: If true, all egress traffic is allowed (ALLOW_ALL policy). + + allow_devbox_to_devbox: If true, allows traffic between the account's own devboxes via tunnels. + + allow_mcp_gateway: If true, allows devbox egress to the MCP hub. + + allowed_hostnames: Updated DNS-based allow list with wildcard support. Examples: ['github.com', + '*.npmjs.org']. + + description: Updated description for the NetworkPolicy. + + name: Updated human-readable name for the NetworkPolicy. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/network-policies/{id}", + body=maybe_transform( + { + "allow_agent_gateway": allow_agent_gateway, + "allow_all": allow_all, + "allow_devbox_to_devbox": allow_devbox_to_devbox, + "allow_mcp_gateway": allow_mcp_gateway, + "allowed_hostnames": allowed_hostnames, + "description": description, + "name": name, + }, + network_policy_update_params.NetworkPolicyUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=NetworkPolicyView, + ) + + def list( + self, + *, + id: str | Omit = omit, + limit: int | Omit = omit, + name: str | Omit = omit, + starting_after: 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, + ) -> SyncNetworkPoliciesCursorIDPage[NetworkPolicyView]: + """ + List all NetworkPolicies for the authenticated account. + + Args: + id: Filter by ID. + + limit: The limit of items to return. Default is 20. Max is 5000. + + name: Filter by name (partial match supported). + + starting_after: Load the next page of data starting after the item with the given ID. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/network-policies", + page=SyncNetworkPoliciesCursorIDPage[NetworkPolicyView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "id": id, + "limit": limit, + "name": name, + "starting_after": starting_after, + }, + network_policy_list_params.NetworkPolicyListParams, + ), + ), + model=NetworkPolicyView, + ) + + 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, + idempotency_key: str | None = None, + ) -> NetworkPolicyView: + """Delete an existing NetworkPolicy. + + This action is irreversible. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/network-policies/{id}/delete", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=NetworkPolicyView, + ) + + +class AsyncNetworkPoliciesResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncNetworkPoliciesResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return AsyncNetworkPoliciesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncNetworkPoliciesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return AsyncNetworkPoliciesResourceWithStreamingResponse(self) + + async def create( + self, + *, + name: str, + allow_agent_gateway: Optional[bool] | Omit = omit, + allow_all: Optional[bool] | Omit = omit, + allow_devbox_to_devbox: Optional[bool] | Omit = omit, + allow_mcp_gateway: Optional[bool] | Omit = omit, + allowed_hostnames: Optional[SequenceNotStr[str]] | Omit = omit, + description: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> NetworkPolicyView: + """Create a new NetworkPolicy with the specified egress rules. + + The policy can then + be applied to blueprints, devboxes, or snapshot resumes. + + Args: + name: The human-readable name for the NetworkPolicy. Must be unique within the + account. + + allow_agent_gateway: (Optional) If true, allows devbox egress to the agent gateway for credential + proxying. Defaults to false. + + allow_all: (Optional) If true, all egress traffic is allowed (ALLOW_ALL policy). Defaults + to false. + + allow_devbox_to_devbox: (Optional) If true, allows traffic between the account's own devboxes via + tunnels. Defaults to false. If allow_all is true, this is automatically set to + true. + + allow_mcp_gateway: (Optional) If true, allows devbox egress to the MCP hub for MCP server access. + Defaults to false. + + allowed_hostnames: (Optional) DNS-based allow list with wildcard support. Examples: ['github.com', + '*.npmjs.org']. + + description: Optional description for the NetworkPolicy. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + return await self._post( + "/v1/network-policies", + body=await async_maybe_transform( + { + "name": name, + "allow_agent_gateway": allow_agent_gateway, + "allow_all": allow_all, + "allow_devbox_to_devbox": allow_devbox_to_devbox, + "allow_mcp_gateway": allow_mcp_gateway, + "allowed_hostnames": allowed_hostnames, + "description": description, + }, + network_policy_create_params.NetworkPolicyCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=NetworkPolicyView, + ) + + 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, + ) -> NetworkPolicyView: + """ + Get a specific NetworkPolicy by its unique identifier. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/v1/network-policies/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NetworkPolicyView, + ) + + async def update( + self, + id: str, + *, + allow_agent_gateway: Optional[bool] | Omit = omit, + allow_all: Optional[bool] | Omit = omit, + allow_devbox_to_devbox: Optional[bool] | Omit = omit, + allow_mcp_gateway: Optional[bool] | Omit = omit, + allowed_hostnames: Optional[SequenceNotStr[str]] | Omit = omit, + description: Optional[str] | Omit = omit, + name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> NetworkPolicyView: + """Update an existing NetworkPolicy. + + All fields are optional. + + Args: + allow_agent_gateway: If true, allows devbox egress to the agent gateway. + + allow_all: If true, all egress traffic is allowed (ALLOW_ALL policy). + + allow_devbox_to_devbox: If true, allows traffic between the account's own devboxes via tunnels. + + allow_mcp_gateway: If true, allows devbox egress to the MCP hub. + + allowed_hostnames: Updated DNS-based allow list with wildcard support. Examples: ['github.com', + '*.npmjs.org']. + + description: Updated description for the NetworkPolicy. + + name: Updated human-readable name for the NetworkPolicy. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/network-policies/{id}", + body=await async_maybe_transform( + { + "allow_agent_gateway": allow_agent_gateway, + "allow_all": allow_all, + "allow_devbox_to_devbox": allow_devbox_to_devbox, + "allow_mcp_gateway": allow_mcp_gateway, + "allowed_hostnames": allowed_hostnames, + "description": description, + "name": name, + }, + network_policy_update_params.NetworkPolicyUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=NetworkPolicyView, + ) + + def list( + self, + *, + id: str | Omit = omit, + limit: int | Omit = omit, + name: str | Omit = omit, + starting_after: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[NetworkPolicyView, AsyncNetworkPoliciesCursorIDPage[NetworkPolicyView]]: + """ + List all NetworkPolicies for the authenticated account. + + Args: + id: Filter by ID. + + limit: The limit of items to return. Default is 20. Max is 5000. + + name: Filter by name (partial match supported). + + starting_after: Load the next page of data starting after the item with the given ID. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/network-policies", + page=AsyncNetworkPoliciesCursorIDPage[NetworkPolicyView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "id": id, + "limit": limit, + "name": name, + "starting_after": starting_after, + }, + network_policy_list_params.NetworkPolicyListParams, + ), + ), + model=NetworkPolicyView, + ) + + 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, + idempotency_key: str | None = None, + ) -> NetworkPolicyView: + """Delete an existing NetworkPolicy. + + This action is irreversible. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/network-policies/{id}/delete", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=NetworkPolicyView, + ) + + +class NetworkPoliciesResourceWithRawResponse: + def __init__(self, network_policies: NetworkPoliciesResource) -> None: + self._network_policies = network_policies + + self.create = to_raw_response_wrapper( + network_policies.create, + ) + self.retrieve = to_raw_response_wrapper( + network_policies.retrieve, + ) + self.update = to_raw_response_wrapper( + network_policies.update, + ) + self.list = to_raw_response_wrapper( + network_policies.list, + ) + self.delete = to_raw_response_wrapper( + network_policies.delete, + ) + + +class AsyncNetworkPoliciesResourceWithRawResponse: + def __init__(self, network_policies: AsyncNetworkPoliciesResource) -> None: + self._network_policies = network_policies + + self.create = async_to_raw_response_wrapper( + network_policies.create, + ) + self.retrieve = async_to_raw_response_wrapper( + network_policies.retrieve, + ) + self.update = async_to_raw_response_wrapper( + network_policies.update, + ) + self.list = async_to_raw_response_wrapper( + network_policies.list, + ) + self.delete = async_to_raw_response_wrapper( + network_policies.delete, + ) + + +class NetworkPoliciesResourceWithStreamingResponse: + def __init__(self, network_policies: NetworkPoliciesResource) -> None: + self._network_policies = network_policies + + self.create = to_streamed_response_wrapper( + network_policies.create, + ) + self.retrieve = to_streamed_response_wrapper( + network_policies.retrieve, + ) + self.update = to_streamed_response_wrapper( + network_policies.update, + ) + self.list = to_streamed_response_wrapper( + network_policies.list, + ) + self.delete = to_streamed_response_wrapper( + network_policies.delete, + ) + + +class AsyncNetworkPoliciesResourceWithStreamingResponse: + def __init__(self, network_policies: AsyncNetworkPoliciesResource) -> None: + self._network_policies = network_policies + + self.create = async_to_streamed_response_wrapper( + network_policies.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + network_policies.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + network_policies.update, + ) + self.list = async_to_streamed_response_wrapper( + network_policies.list, + ) + self.delete = async_to_streamed_response_wrapper( + network_policies.delete, + ) diff --git a/src/runloop_api_client/resources/objects.py b/src/runloop_api_client/resources/objects.py new file mode 100644 index 000000000..409d5f6f3 --- /dev/null +++ b/src/runloop_api_client/resources/objects.py @@ -0,0 +1,870 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, Optional +from typing_extensions import Literal + +import httpx + +from ..types import object_list_params, object_create_params, object_download_params, object_list_public_params +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..pagination import SyncObjectsCursorIDPage, AsyncObjectsCursorIDPage +from .._base_client import AsyncPaginator, make_request_options +from ..types.object_view import ObjectView +from ..types.object_download_url_view import ObjectDownloadURLView + +__all__ = ["ObjectsResource", "AsyncObjectsResource"] + + +class ObjectsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> ObjectsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return ObjectsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ObjectsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return ObjectsResourceWithStreamingResponse(self) + + def create( + self, + *, + content_type: Literal["unspecified", "text", "binary", "gzip", "tar", "tgz"], + name: str, + metadata: Optional[Dict[str, str]] | Omit = omit, + ttl_ms: 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, + idempotency_key: str | None = None, + ) -> ObjectView: + """Create a new Object with content and metadata. + + The Object will be assigned a + unique ID. + + Args: + content_type: The content type of the Object. + + name: The name of the Object. + + metadata: User defined metadata to attach to the object for organization. + + ttl_ms: Optional lifetime of the object in milliseconds, after which the object is + automatically deleted. Time starts ticking after the object is created. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + return self._post( + "/v1/objects", + body=maybe_transform( + { + "content_type": content_type, + "name": name, + "metadata": metadata, + "ttl_ms": ttl_ms, + }, + object_create_params.ObjectCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ObjectView, + ) + + 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, + ) -> ObjectView: + """ + Retrieve a specific Object by its unique identifier. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/v1/objects/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ObjectView, + ) + + def list( + self, + *, + content_type: Literal["unspecified", "text", "binary", "gzip", "tar", "tgz"] | Omit = omit, + limit: int | Omit = omit, + name: str | Omit = omit, + search: str | Omit = omit, + starting_after: str | Omit = omit, + state: Literal["UPLOADING", "READ_ONLY", "DELETED", "ERROR"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SyncObjectsCursorIDPage[ObjectView]: + """ + List all Objects for the authenticated account with pagination support. + + Args: + content_type: Filter storage objects by content type. + + limit: The limit of items to return. Default is 20. Max is 5000. + + name: Filter storage objects by name (partial match supported). + + search: Search by object ID or name. + + starting_after: Load the next page of data starting after the item with the given ID. + + state: Filter storage objects by state. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters 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( + "/v1/objects", + page=SyncObjectsCursorIDPage[ObjectView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "content_type": content_type, + "limit": limit, + "name": name, + "search": search, + "starting_after": starting_after, + "state": state, + }, + object_list_params.ObjectListParams, + ), + ), + model=ObjectView, + ) + + 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, + idempotency_key: str | None = None, + ) -> ObjectView: + """Delete an existing Object by ID. + + This action is irreversible and will remove the + Object and all its metadata. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/objects/{id}/delete", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ObjectView, + ) + + def complete( + 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, + idempotency_key: str | None = None, + ) -> ObjectView: + """ + Mark an Object's upload as complete, transitioning it from UPLOADING to + READ-only 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 + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/objects/{id}/complete", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ObjectView, + ) + + def download( + self, + id: str, + *, + duration_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, + ) -> ObjectDownloadURLView: + """Generate a presigned download URL for an Object. + + The URL will be valid for the + specified duration. + + Args: + duration_seconds: Duration in seconds for the presigned URL validity (default: 3600). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/v1/objects/{id}/download", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + {"duration_seconds": duration_seconds}, object_download_params.ObjectDownloadParams + ), + ), + cast_to=ObjectDownloadURLView, + ) + + def list_public( + self, + *, + content_type: Literal["unspecified", "text", "binary", "gzip", "tar", "tgz"] | Omit = omit, + limit: int | Omit = omit, + name: str | Omit = omit, + search: str | Omit = omit, + starting_after: str | Omit = omit, + state: Literal["UPLOADING", "READ_ONLY", "DELETED", "ERROR"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SyncObjectsCursorIDPage[ObjectView]: + """ + List all public Objects with pagination support. + + Args: + content_type: Filter storage objects by content type. + + limit: The limit of items to return. Default is 20. Max is 5000. + + name: Filter storage objects by name (partial match supported). + + search: Search by object ID or name. + + starting_after: Load the next page of data starting after the item with the given ID. + + state: Filter storage objects by state. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters 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( + "/v1/objects/list_public", + page=SyncObjectsCursorIDPage[ObjectView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "content_type": content_type, + "limit": limit, + "name": name, + "search": search, + "starting_after": starting_after, + "state": state, + }, + object_list_public_params.ObjectListPublicParams, + ), + ), + model=ObjectView, + ) + + +class AsyncObjectsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncObjectsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return AsyncObjectsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncObjectsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return AsyncObjectsResourceWithStreamingResponse(self) + + async def create( + self, + *, + content_type: Literal["unspecified", "text", "binary", "gzip", "tar", "tgz"], + name: str, + metadata: Optional[Dict[str, str]] | Omit = omit, + ttl_ms: 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, + idempotency_key: str | None = None, + ) -> ObjectView: + """Create a new Object with content and metadata. + + The Object will be assigned a + unique ID. + + Args: + content_type: The content type of the Object. + + name: The name of the Object. + + metadata: User defined metadata to attach to the object for organization. + + ttl_ms: Optional lifetime of the object in milliseconds, after which the object is + automatically deleted. Time starts ticking after the object is created. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + return await self._post( + "/v1/objects", + body=await async_maybe_transform( + { + "content_type": content_type, + "name": name, + "metadata": metadata, + "ttl_ms": ttl_ms, + }, + object_create_params.ObjectCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ObjectView, + ) + + 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, + ) -> ObjectView: + """ + Retrieve a specific Object by its unique identifier. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/v1/objects/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ObjectView, + ) + + def list( + self, + *, + content_type: Literal["unspecified", "text", "binary", "gzip", "tar", "tgz"] | Omit = omit, + limit: int | Omit = omit, + name: str | Omit = omit, + search: str | Omit = omit, + starting_after: str | Omit = omit, + state: Literal["UPLOADING", "READ_ONLY", "DELETED", "ERROR"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this 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[ObjectView, AsyncObjectsCursorIDPage[ObjectView]]: + """ + List all Objects for the authenticated account with pagination support. + + Args: + content_type: Filter storage objects by content type. + + limit: The limit of items to return. Default is 20. Max is 5000. + + name: Filter storage objects by name (partial match supported). + + search: Search by object ID or name. + + starting_after: Load the next page of data starting after the item with the given ID. + + state: Filter storage objects by state. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters 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( + "/v1/objects", + page=AsyncObjectsCursorIDPage[ObjectView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "content_type": content_type, + "limit": limit, + "name": name, + "search": search, + "starting_after": starting_after, + "state": state, + }, + object_list_params.ObjectListParams, + ), + ), + model=ObjectView, + ) + + 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, + idempotency_key: str | None = None, + ) -> ObjectView: + """Delete an existing Object by ID. + + This action is irreversible and will remove the + Object and all its metadata. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/objects/{id}/delete", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ObjectView, + ) + + async def complete( + 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, + idempotency_key: str | None = None, + ) -> ObjectView: + """ + Mark an Object's upload as complete, transitioning it from UPLOADING to + READ-only 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 + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/objects/{id}/complete", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ObjectView, + ) + + async def download( + self, + id: str, + *, + duration_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, + ) -> ObjectDownloadURLView: + """Generate a presigned download URL for an Object. + + The URL will be valid for the + specified duration. + + Args: + duration_seconds: Duration in seconds for the presigned URL validity (default: 3600). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/v1/objects/{id}/download", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + {"duration_seconds": duration_seconds}, object_download_params.ObjectDownloadParams + ), + ), + cast_to=ObjectDownloadURLView, + ) + + def list_public( + self, + *, + content_type: Literal["unspecified", "text", "binary", "gzip", "tar", "tgz"] | Omit = omit, + limit: int | Omit = omit, + name: str | Omit = omit, + search: str | Omit = omit, + starting_after: str | Omit = omit, + state: Literal["UPLOADING", "READ_ONLY", "DELETED", "ERROR"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this 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[ObjectView, AsyncObjectsCursorIDPage[ObjectView]]: + """ + List all public Objects with pagination support. + + Args: + content_type: Filter storage objects by content type. + + limit: The limit of items to return. Default is 20. Max is 5000. + + name: Filter storage objects by name (partial match supported). + + search: Search by object ID or name. + + starting_after: Load the next page of data starting after the item with the given ID. + + state: Filter storage objects by state. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters 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( + "/v1/objects/list_public", + page=AsyncObjectsCursorIDPage[ObjectView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "content_type": content_type, + "limit": limit, + "name": name, + "search": search, + "starting_after": starting_after, + "state": state, + }, + object_list_public_params.ObjectListPublicParams, + ), + ), + model=ObjectView, + ) + + +class ObjectsResourceWithRawResponse: + def __init__(self, objects: ObjectsResource) -> None: + self._objects = objects + + self.create = to_raw_response_wrapper( + objects.create, + ) + self.retrieve = to_raw_response_wrapper( + objects.retrieve, + ) + self.list = to_raw_response_wrapper( + objects.list, + ) + self.delete = to_raw_response_wrapper( + objects.delete, + ) + self.complete = to_raw_response_wrapper( + objects.complete, + ) + self.download = to_raw_response_wrapper( + objects.download, + ) + self.list_public = to_raw_response_wrapper( + objects.list_public, + ) + + +class AsyncObjectsResourceWithRawResponse: + def __init__(self, objects: AsyncObjectsResource) -> None: + self._objects = objects + + self.create = async_to_raw_response_wrapper( + objects.create, + ) + self.retrieve = async_to_raw_response_wrapper( + objects.retrieve, + ) + self.list = async_to_raw_response_wrapper( + objects.list, + ) + self.delete = async_to_raw_response_wrapper( + objects.delete, + ) + self.complete = async_to_raw_response_wrapper( + objects.complete, + ) + self.download = async_to_raw_response_wrapper( + objects.download, + ) + self.list_public = async_to_raw_response_wrapper( + objects.list_public, + ) + + +class ObjectsResourceWithStreamingResponse: + def __init__(self, objects: ObjectsResource) -> None: + self._objects = objects + + self.create = to_streamed_response_wrapper( + objects.create, + ) + self.retrieve = to_streamed_response_wrapper( + objects.retrieve, + ) + self.list = to_streamed_response_wrapper( + objects.list, + ) + self.delete = to_streamed_response_wrapper( + objects.delete, + ) + self.complete = to_streamed_response_wrapper( + objects.complete, + ) + self.download = to_streamed_response_wrapper( + objects.download, + ) + self.list_public = to_streamed_response_wrapper( + objects.list_public, + ) + + +class AsyncObjectsResourceWithStreamingResponse: + def __init__(self, objects: AsyncObjectsResource) -> None: + self._objects = objects + + self.create = async_to_streamed_response_wrapper( + objects.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + objects.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + objects.list, + ) + self.delete = async_to_streamed_response_wrapper( + objects.delete, + ) + self.complete = async_to_streamed_response_wrapper( + objects.complete, + ) + self.download = async_to_streamed_response_wrapper( + objects.download, + ) + self.list_public = async_to_streamed_response_wrapper( + objects.list_public, + ) diff --git a/src/runloop_api_client/resources/repositories.py b/src/runloop_api_client/resources/repositories.py new file mode 100644 index 000000000..a22075540 --- /dev/null +++ b/src/runloop_api_client/resources/repositories.py @@ -0,0 +1,918 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import typing_extensions +from typing import Optional + +import httpx + +from ..types import ( + repository_list_params, + repository_create_params, + repository_inspect_params, + repository_refresh_params, +) +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..pagination import SyncRepositoriesCursorIDPage, AsyncRepositoriesCursorIDPage +from .._base_client import AsyncPaginator, make_request_options +from ..types.repository_connection_view import RepositoryConnectionView +from ..types.repository_inspection_details import RepositoryInspectionDetails +from ..types.repository_inspection_list_view import RepositoryInspectionListView + +__all__ = ["RepositoriesResource", "AsyncRepositoriesResource"] + + +class RepositoriesResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> RepositoriesResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return RepositoriesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> RepositoriesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return RepositoriesResourceWithStreamingResponse(self) + + def create( + self, + *, + name: str, + owner: str, + blueprint_id: Optional[str] | Omit = omit, + github_auth_token: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> RepositoryConnectionView: + """ + Create a connection to a Github Repository and trigger an initial inspection of + the repo's technical stack and developer environment requirements. + + Args: + name: Name of the repository. + + owner: Account owner of the repository. + + blueprint_id: ID of blueprint to use as base for resulting RepositoryVersion blueprint. + + github_auth_token: GitHub authentication token for accessing private repositories. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + return self._post( + "/v1/repositories", + body=maybe_transform( + { + "name": name, + "owner": owner, + "blueprint_id": blueprint_id, + "github_auth_token": github_auth_token, + }, + repository_create_params.RepositoryCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=RepositoryConnectionView, + ) + + 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, + ) -> RepositoryConnectionView: + """ + Get Repository Connection details including latest inspection status and + generated repository insights. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/v1/repositories/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=RepositoryConnectionView, + ) + + def list( + self, + *, + limit: int | Omit = omit, + name: str | Omit = omit, + owner: str | Omit = omit, + starting_after: 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, + ) -> SyncRepositoriesCursorIDPage[RepositoryConnectionView]: + """ + List all available repository connections. + + Args: + limit: The limit of items to return. Default is 20. Max is 5000. + + name: Filter by repository name + + owner: Filter by repository owner + + starting_after: Load the next page of data starting after the item with the given ID. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/repositories", + page=SyncRepositoriesCursorIDPage[RepositoryConnectionView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "name": name, + "owner": owner, + "starting_after": starting_after, + }, + repository_list_params.RepositoryListParams, + ), + ), + model=RepositoryConnectionView, + ) + + 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, + idempotency_key: str | None = None, + ) -> object: + """ + Permanently Delete a Repository Connection including any automatically generated + inspection insights. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/repositories/{id}/delete", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=object, + ) + + def inspect( + self, + id: str, + *, + github_auth_token: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> RepositoryInspectionDetails: + """ + Inspect a repository connection by inspecting the specified version including + repo's technical stack and developer environment requirements. + + Args: + github_auth_token: GitHub authentication token for accessing private repositories. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/repositories/{id}/inspect", + body=maybe_transform( + {"github_auth_token": github_auth_token}, repository_inspect_params.RepositoryInspectParams + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=RepositoryInspectionDetails, + ) + + def list_inspections( + 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, + ) -> RepositoryInspectionListView: + """ + List all inspections of a repository connection including automatically + generated insights for each inspection. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/v1/repositories/{id}/inspections", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=RepositoryInspectionListView, + ) + + @typing_extensions.deprecated("deprecated") + def refresh( + self, + id: str, + *, + blueprint_id: Optional[str] | Omit = omit, + github_auth_token: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> object: + """ + Refresh a repository connection by inspecting the latest version including + repo's technical stack and developer environment requirements. + + Args: + blueprint_id: ID of blueprint to use as base for resulting RepositoryVersion blueprint. + + github_auth_token: GitHub authentication token for accessing private repositories. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/repositories/{id}/refresh", + body=maybe_transform( + { + "blueprint_id": blueprint_id, + "github_auth_token": github_auth_token, + }, + repository_refresh_params.RepositoryRefreshParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=object, + ) + + def retrieve_inspection( + 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, + ) -> RepositoryInspectionDetails: + """ + Get a repository inspection 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"/v1/repositories/inspections/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=RepositoryInspectionDetails, + ) + + +class AsyncRepositoriesResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncRepositoriesResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return AsyncRepositoriesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncRepositoriesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return AsyncRepositoriesResourceWithStreamingResponse(self) + + async def create( + self, + *, + name: str, + owner: str, + blueprint_id: Optional[str] | Omit = omit, + github_auth_token: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> RepositoryConnectionView: + """ + Create a connection to a Github Repository and trigger an initial inspection of + the repo's technical stack and developer environment requirements. + + Args: + name: Name of the repository. + + owner: Account owner of the repository. + + blueprint_id: ID of blueprint to use as base for resulting RepositoryVersion blueprint. + + github_auth_token: GitHub authentication token for accessing private repositories. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + return await self._post( + "/v1/repositories", + body=await async_maybe_transform( + { + "name": name, + "owner": owner, + "blueprint_id": blueprint_id, + "github_auth_token": github_auth_token, + }, + repository_create_params.RepositoryCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=RepositoryConnectionView, + ) + + 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, + ) -> RepositoryConnectionView: + """ + Get Repository Connection details including latest inspection status and + generated repository insights. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/v1/repositories/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=RepositoryConnectionView, + ) + + def list( + self, + *, + limit: int | Omit = omit, + name: str | Omit = omit, + owner: str | Omit = omit, + starting_after: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[RepositoryConnectionView, AsyncRepositoriesCursorIDPage[RepositoryConnectionView]]: + """ + List all available repository connections. + + Args: + limit: The limit of items to return. Default is 20. Max is 5000. + + name: Filter by repository name + + owner: Filter by repository owner + + starting_after: Load the next page of data starting after the item with the given ID. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/repositories", + page=AsyncRepositoriesCursorIDPage[RepositoryConnectionView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "name": name, + "owner": owner, + "starting_after": starting_after, + }, + repository_list_params.RepositoryListParams, + ), + ), + model=RepositoryConnectionView, + ) + + 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, + idempotency_key: str | None = None, + ) -> object: + """ + Permanently Delete a Repository Connection including any automatically generated + inspection insights. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/repositories/{id}/delete", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=object, + ) + + async def inspect( + self, + id: str, + *, + github_auth_token: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> RepositoryInspectionDetails: + """ + Inspect a repository connection by inspecting the specified version including + repo's technical stack and developer environment requirements. + + Args: + github_auth_token: GitHub authentication token for accessing private repositories. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/repositories/{id}/inspect", + body=await async_maybe_transform( + {"github_auth_token": github_auth_token}, repository_inspect_params.RepositoryInspectParams + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=RepositoryInspectionDetails, + ) + + async def list_inspections( + 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, + ) -> RepositoryInspectionListView: + """ + List all inspections of a repository connection including automatically + generated insights for each inspection. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/v1/repositories/{id}/inspections", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=RepositoryInspectionListView, + ) + + @typing_extensions.deprecated("deprecated") + async def refresh( + self, + id: str, + *, + blueprint_id: Optional[str] | Omit = omit, + github_auth_token: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> object: + """ + Refresh a repository connection by inspecting the latest version including + repo's technical stack and developer environment requirements. + + Args: + blueprint_id: ID of blueprint to use as base for resulting RepositoryVersion blueprint. + + github_auth_token: GitHub authentication token for accessing private repositories. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/repositories/{id}/refresh", + body=await async_maybe_transform( + { + "blueprint_id": blueprint_id, + "github_auth_token": github_auth_token, + }, + repository_refresh_params.RepositoryRefreshParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=object, + ) + + async def retrieve_inspection( + 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, + ) -> RepositoryInspectionDetails: + """ + Get a repository inspection 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"/v1/repositories/inspections/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=RepositoryInspectionDetails, + ) + + +class RepositoriesResourceWithRawResponse: + def __init__(self, repositories: RepositoriesResource) -> None: + self._repositories = repositories + + self.create = to_raw_response_wrapper( + repositories.create, + ) + self.retrieve = to_raw_response_wrapper( + repositories.retrieve, + ) + self.list = to_raw_response_wrapper( + repositories.list, + ) + self.delete = to_raw_response_wrapper( + repositories.delete, + ) + self.inspect = to_raw_response_wrapper( + repositories.inspect, + ) + self.list_inspections = to_raw_response_wrapper( + repositories.list_inspections, + ) + self.refresh = ( # pyright: ignore[reportDeprecated] + to_raw_response_wrapper( + repositories.refresh, # pyright: ignore[reportDeprecated], + ) + ) + self.retrieve_inspection = to_raw_response_wrapper( + repositories.retrieve_inspection, + ) + + +class AsyncRepositoriesResourceWithRawResponse: + def __init__(self, repositories: AsyncRepositoriesResource) -> None: + self._repositories = repositories + + self.create = async_to_raw_response_wrapper( + repositories.create, + ) + self.retrieve = async_to_raw_response_wrapper( + repositories.retrieve, + ) + self.list = async_to_raw_response_wrapper( + repositories.list, + ) + self.delete = async_to_raw_response_wrapper( + repositories.delete, + ) + self.inspect = async_to_raw_response_wrapper( + repositories.inspect, + ) + self.list_inspections = async_to_raw_response_wrapper( + repositories.list_inspections, + ) + self.refresh = ( # pyright: ignore[reportDeprecated] + async_to_raw_response_wrapper( + repositories.refresh, # pyright: ignore[reportDeprecated], + ) + ) + self.retrieve_inspection = async_to_raw_response_wrapper( + repositories.retrieve_inspection, + ) + + +class RepositoriesResourceWithStreamingResponse: + def __init__(self, repositories: RepositoriesResource) -> None: + self._repositories = repositories + + self.create = to_streamed_response_wrapper( + repositories.create, + ) + self.retrieve = to_streamed_response_wrapper( + repositories.retrieve, + ) + self.list = to_streamed_response_wrapper( + repositories.list, + ) + self.delete = to_streamed_response_wrapper( + repositories.delete, + ) + self.inspect = to_streamed_response_wrapper( + repositories.inspect, + ) + self.list_inspections = to_streamed_response_wrapper( + repositories.list_inspections, + ) + self.refresh = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + repositories.refresh, # pyright: ignore[reportDeprecated], + ) + ) + self.retrieve_inspection = to_streamed_response_wrapper( + repositories.retrieve_inspection, + ) + + +class AsyncRepositoriesResourceWithStreamingResponse: + def __init__(self, repositories: AsyncRepositoriesResource) -> None: + self._repositories = repositories + + self.create = async_to_streamed_response_wrapper( + repositories.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + repositories.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + repositories.list, + ) + self.delete = async_to_streamed_response_wrapper( + repositories.delete, + ) + self.inspect = async_to_streamed_response_wrapper( + repositories.inspect, + ) + self.list_inspections = async_to_streamed_response_wrapper( + repositories.list_inspections, + ) + self.refresh = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + repositories.refresh, # pyright: ignore[reportDeprecated], + ) + ) + self.retrieve_inspection = async_to_streamed_response_wrapper( + repositories.retrieve_inspection, + ) diff --git a/src/runloop_api_client/resources/scenarios/__init__.py b/src/runloop_api_client/resources/scenarios/__init__.py new file mode 100644 index 000000000..4d6bc16cf --- /dev/null +++ b/src/runloop_api_client/resources/scenarios/__init__.py @@ -0,0 +1,47 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .runs import ( + RunsResource, + AsyncRunsResource, + RunsResourceWithRawResponse, + AsyncRunsResourceWithRawResponse, + RunsResourceWithStreamingResponse, + AsyncRunsResourceWithStreamingResponse, +) +from .scorers import ( + ScorersResource, + AsyncScorersResource, + ScorersResourceWithRawResponse, + AsyncScorersResourceWithRawResponse, + ScorersResourceWithStreamingResponse, + AsyncScorersResourceWithStreamingResponse, +) +from .scenarios import ( + ScenariosResource, + AsyncScenariosResource, + ScenariosResourceWithRawResponse, + AsyncScenariosResourceWithRawResponse, + ScenariosResourceWithStreamingResponse, + AsyncScenariosResourceWithStreamingResponse, +) + +__all__ = [ + "RunsResource", + "AsyncRunsResource", + "RunsResourceWithRawResponse", + "AsyncRunsResourceWithRawResponse", + "RunsResourceWithStreamingResponse", + "AsyncRunsResourceWithStreamingResponse", + "ScorersResource", + "AsyncScorersResource", + "ScorersResourceWithRawResponse", + "AsyncScorersResourceWithRawResponse", + "ScorersResourceWithStreamingResponse", + "AsyncScorersResourceWithStreamingResponse", + "ScenariosResource", + "AsyncScenariosResource", + "ScenariosResourceWithRawResponse", + "AsyncScenariosResourceWithRawResponse", + "ScenariosResourceWithStreamingResponse", + "AsyncScenariosResourceWithStreamingResponse", +] diff --git a/src/runloop_api_client/resources/scenarios/runs.py b/src/runloop_api_client/resources/scenarios/runs.py new file mode 100644 index 000000000..faa042747 --- /dev/null +++ b/src/runloop_api_client/resources/scenarios/runs.py @@ -0,0 +1,696 @@ +# 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 +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 ...pagination import SyncBenchmarkRunsCursorIDPage, AsyncBenchmarkRunsCursorIDPage +from ..._base_client import AsyncPaginator, make_request_options +from ...types.scenarios import run_list_params +from ...types.scenario_run_view import ScenarioRunView + +__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/runloopai/api-client-python#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/runloopai/api-client-python#with_streaming_response + """ + return RunsResourceWithStreamingResponse(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, + ) -> ScenarioRunView: + """ + Get a ScenarioRun given 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"/v1/scenarios/runs/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ScenarioRunView, + ) + + def list( + self, + *, + benchmark_run_id: str | Omit = omit, + limit: int | Omit = omit, + name: str | Omit = omit, + scenario_id: str | Omit = omit, + starting_after: str | Omit = omit, + state: 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, + ) -> SyncBenchmarkRunsCursorIDPage[ScenarioRunView]: + """ + List all ScenarioRuns matching filter. + + Args: + benchmark_run_id: Filter by benchmark run ID + + limit: The limit of items to return. Default is 20. Max is 5000. + + name: Filter by name + + scenario_id: Filter runs associated to Scenario given ID + + starting_after: Load the next page of data starting after the item with the given ID. + + state: Filter by state + + extra_headers: Send extra headers + + extra_query: Add additional query parameters 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( + "/v1/scenarios/runs", + page=SyncBenchmarkRunsCursorIDPage[ScenarioRunView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "benchmark_run_id": benchmark_run_id, + "limit": limit, + "name": name, + "scenario_id": scenario_id, + "starting_after": starting_after, + "state": state, + }, + run_list_params.RunListParams, + ), + ), + model=ScenarioRunView, + ) + + def cancel( + 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, + idempotency_key: str | None = None, + ) -> ScenarioRunView: + """Cancel a currently running Scenario run. + + This will shutdown the underlying + Devbox resource. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/scenarios/runs/{id}/cancel", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ScenarioRunView, + ) + + def complete( + 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, + idempotency_key: str | None = None, + ) -> ScenarioRunView: + """Complete a currently running ScenarioRun. + + Calling complete will shutdown + underlying Devbox resource. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/scenarios/runs/{id}/complete", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ScenarioRunView, + ) + + def download_logs( + 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, + idempotency_key: str | None = None, + ) -> BinaryAPIResponse: + """ + Download a zip file containing all logs for a Scenario run from the associated + devbox. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + 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._post( + f"/v1/scenarios/runs/{id}/download_logs", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=BinaryAPIResponse, + ) + + def score( + 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, + idempotency_key: str | None = None, + ) -> ScenarioRunView: + """ + Score a currently running ScenarioRun. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/scenarios/runs/{id}/score", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ScenarioRunView, + ) + + +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/runloopai/api-client-python#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/runloopai/api-client-python#with_streaming_response + """ + return AsyncRunsResourceWithStreamingResponse(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, + ) -> ScenarioRunView: + """ + Get a ScenarioRun given 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"/v1/scenarios/runs/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ScenarioRunView, + ) + + def list( + self, + *, + benchmark_run_id: str | Omit = omit, + limit: int | Omit = omit, + name: str | Omit = omit, + scenario_id: str | Omit = omit, + starting_after: str | Omit = omit, + state: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[ScenarioRunView, AsyncBenchmarkRunsCursorIDPage[ScenarioRunView]]: + """ + List all ScenarioRuns matching filter. + + Args: + benchmark_run_id: Filter by benchmark run ID + + limit: The limit of items to return. Default is 20. Max is 5000. + + name: Filter by name + + scenario_id: Filter runs associated to Scenario given ID + + starting_after: Load the next page of data starting after the item with the given ID. + + state: Filter by state + + extra_headers: Send extra headers + + extra_query: Add additional query parameters 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( + "/v1/scenarios/runs", + page=AsyncBenchmarkRunsCursorIDPage[ScenarioRunView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "benchmark_run_id": benchmark_run_id, + "limit": limit, + "name": name, + "scenario_id": scenario_id, + "starting_after": starting_after, + "state": state, + }, + run_list_params.RunListParams, + ), + ), + model=ScenarioRunView, + ) + + async def cancel( + 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, + idempotency_key: str | None = None, + ) -> ScenarioRunView: + """Cancel a currently running Scenario run. + + This will shutdown the underlying + Devbox resource. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/scenarios/runs/{id}/cancel", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ScenarioRunView, + ) + + async def complete( + 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, + idempotency_key: str | None = None, + ) -> ScenarioRunView: + """Complete a currently running ScenarioRun. + + Calling complete will shutdown + underlying Devbox resource. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/scenarios/runs/{id}/complete", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ScenarioRunView, + ) + + async def download_logs( + 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, + idempotency_key: str | None = None, + ) -> AsyncBinaryAPIResponse: + """ + Download a zip file containing all logs for a Scenario run from the associated + devbox. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + 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._post( + f"/v1/scenarios/runs/{id}/download_logs", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=AsyncBinaryAPIResponse, + ) + + async def score( + 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, + idempotency_key: str | None = None, + ) -> ScenarioRunView: + """ + Score a currently running ScenarioRun. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/scenarios/runs/{id}/score", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ScenarioRunView, + ) + + +class RunsResourceWithRawResponse: + def __init__(self, runs: RunsResource) -> None: + self._runs = runs + + self.retrieve = to_raw_response_wrapper( + runs.retrieve, + ) + self.list = to_raw_response_wrapper( + runs.list, + ) + self.cancel = to_raw_response_wrapper( + runs.cancel, + ) + self.complete = to_raw_response_wrapper( + runs.complete, + ) + self.download_logs = to_custom_raw_response_wrapper( + runs.download_logs, + BinaryAPIResponse, + ) + self.score = to_raw_response_wrapper( + runs.score, + ) + + +class AsyncRunsResourceWithRawResponse: + def __init__(self, runs: AsyncRunsResource) -> None: + self._runs = runs + + self.retrieve = async_to_raw_response_wrapper( + runs.retrieve, + ) + self.list = async_to_raw_response_wrapper( + runs.list, + ) + self.cancel = async_to_raw_response_wrapper( + runs.cancel, + ) + self.complete = async_to_raw_response_wrapper( + runs.complete, + ) + self.download_logs = async_to_custom_raw_response_wrapper( + runs.download_logs, + AsyncBinaryAPIResponse, + ) + self.score = async_to_raw_response_wrapper( + runs.score, + ) + + +class RunsResourceWithStreamingResponse: + def __init__(self, runs: RunsResource) -> None: + self._runs = runs + + self.retrieve = to_streamed_response_wrapper( + runs.retrieve, + ) + self.list = to_streamed_response_wrapper( + runs.list, + ) + self.cancel = to_streamed_response_wrapper( + runs.cancel, + ) + self.complete = to_streamed_response_wrapper( + runs.complete, + ) + self.download_logs = to_custom_streamed_response_wrapper( + runs.download_logs, + StreamedBinaryAPIResponse, + ) + self.score = to_streamed_response_wrapper( + runs.score, + ) + + +class AsyncRunsResourceWithStreamingResponse: + def __init__(self, runs: AsyncRunsResource) -> None: + self._runs = runs + + self.retrieve = async_to_streamed_response_wrapper( + runs.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + runs.list, + ) + self.cancel = async_to_streamed_response_wrapper( + runs.cancel, + ) + self.complete = async_to_streamed_response_wrapper( + runs.complete, + ) + self.download_logs = async_to_custom_streamed_response_wrapper( + runs.download_logs, + AsyncStreamedBinaryAPIResponse, + ) + self.score = async_to_streamed_response_wrapper( + runs.score, + ) diff --git a/src/runloop_api_client/resources/scenarios/scenarios.py b/src/runloop_api_client/resources/scenarios/scenarios.py new file mode 100644 index 000000000..3eb4849f6 --- /dev/null +++ b/src/runloop_api_client/resources/scenarios/scenarios.py @@ -0,0 +1,1102 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, Optional +from typing_extensions import Literal + +import httpx + +from .runs import ( + RunsResource, + AsyncRunsResource, + RunsResourceWithRawResponse, + AsyncRunsResourceWithRawResponse, + RunsResourceWithStreamingResponse, + AsyncRunsResourceWithStreamingResponse, +) +from ...types import ( + scenario_list_params, + scenario_create_params, + scenario_update_params, + scenario_start_run_params, + scenario_list_public_params, +) +from .scorers import ( + ScorersResource, + AsyncScorersResource, + ScorersResourceWithRawResponse, + AsyncScorersResourceWithRawResponse, + ScorersResourceWithStreamingResponse, + AsyncScorersResourceWithStreamingResponse, +) +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 +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ...pagination import SyncScenariosCursorIDPage, AsyncScenariosCursorIDPage +from ..._base_client import AsyncPaginator, make_request_options +from ...types.scenario_view import ScenarioView +from ...types.scenario_run_view import ScenarioRunView +from ...types.input_context_param import InputContextParam +from ...types.scoring_contract_param import ScoringContractParam +from ...types.shared_params.run_profile import RunProfile +from ...types.input_context_update_param import InputContextUpdateParam +from ...types.scenario_environment_param import ScenarioEnvironmentParam +from ...types.scoring_contract_update_param import ScoringContractUpdateParam + +__all__ = ["ScenariosResource", "AsyncScenariosResource"] + + +class ScenariosResource(SyncAPIResource): + @cached_property + def runs(self) -> RunsResource: + return RunsResource(self._client) + + @cached_property + def scorers(self) -> ScorersResource: + return ScorersResource(self._client) + + @cached_property + def with_raw_response(self) -> ScenariosResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return ScenariosResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ScenariosResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return ScenariosResourceWithStreamingResponse(self) + + def create( + self, + *, + input_context: InputContextParam, + name: str, + scoring_contract: ScoringContractParam, + environment_parameters: Optional[ScenarioEnvironmentParam] | Omit = omit, + metadata: Optional[Dict[str, str]] | Omit = omit, + reference_output: Optional[str] | Omit = omit, + required_environment_variables: Optional[SequenceNotStr[str]] | Omit = omit, + required_secret_names: Optional[SequenceNotStr[str]] | Omit = omit, + scorer_timeout_sec: Optional[int] | Omit = omit, + validation_type: Optional[Literal["UNSPECIFIED", "FORWARD", "REVERSE", "EVALUATION"]] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> ScenarioView: + """ + Create a Scenario, a repeatable AI coding evaluation test that defines the + starting environment as well as evaluation success criteria. + + Args: + input_context: The input context for the Scenario. + + name: Name of the scenario. + + scoring_contract: The scoring contract for the Scenario. + + environment_parameters: The Environment in which the Scenario will run. + + metadata: User defined metadata to attach to the scenario for organization. + + reference_output: A string representation of the reference output to solve the scenario. Commonly + can be the result of a git diff or a sequence of command actions to apply to the + environment. + + required_environment_variables: Environment variables required to run the scenario. If these variables are not + provided, the scenario will fail to start. + + required_secret_names: Secrets required to run the scenario (user secret name to scenario required + secret name). If these secrets are not provided or the mapping is incorrect, the + scenario will fail to start. + + scorer_timeout_sec: Timeout for scoring in seconds. Default 30 minutes (1800s). + + validation_type: Validation strategy. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + return self._post( + "/v1/scenarios", + body=maybe_transform( + { + "input_context": input_context, + "name": name, + "scoring_contract": scoring_contract, + "environment_parameters": environment_parameters, + "metadata": metadata, + "reference_output": reference_output, + "required_environment_variables": required_environment_variables, + "required_secret_names": required_secret_names, + "scorer_timeout_sec": scorer_timeout_sec, + "validation_type": validation_type, + }, + scenario_create_params.ScenarioCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ScenarioView, + ) + + 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, + ) -> ScenarioView: + """ + Get a previously created scenario. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/v1/scenarios/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ScenarioView, + ) + + def update( + self, + id: str, + *, + environment_parameters: Optional[ScenarioEnvironmentParam] | Omit = omit, + input_context: Optional[InputContextUpdateParam] | Omit = omit, + metadata: Optional[Dict[str, str]] | Omit = omit, + name: Optional[str] | Omit = omit, + reference_output: Optional[str] | Omit = omit, + required_environment_variables: Optional[SequenceNotStr[str]] | Omit = omit, + required_secret_names: Optional[SequenceNotStr[str]] | Omit = omit, + scorer_timeout_sec: Optional[int] | Omit = omit, + scoring_contract: Optional[ScoringContractUpdateParam] | Omit = omit, + validation_type: Optional[Literal["UNSPECIFIED", "FORWARD", "REVERSE", "EVALUATION"]] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> ScenarioView: + """Update a Scenario. + + Fields that are null will preserve the existing value. Fields + that are provided (including empty values) will replace the existing value + entirely. + + Args: + environment_parameters: The Environment in which the Scenario will run. + + input_context: The input context for the Scenario. + + metadata: User defined metadata to attach to the scenario. Pass in empty map to clear. + + name: Name of the scenario. Cannot be blank. + + reference_output: A string representation of the reference output to solve the scenario. Commonly + can be the result of a git diff or a sequence of command actions to apply to the + environment. Pass in empty string to clear. + + required_environment_variables: Environment variables required to run the scenario. Pass in empty list to clear. + + required_secret_names: Secrets required to run the scenario. Pass in empty list to clear. + + scorer_timeout_sec: Timeout for scoring in seconds. Default 30 minutes (1800s). + + scoring_contract: The scoring contract for the Scenario. + + validation_type: Validation strategy. Pass in empty string to clear. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/scenarios/{id}", + body=maybe_transform( + { + "environment_parameters": environment_parameters, + "input_context": input_context, + "metadata": metadata, + "name": name, + "reference_output": reference_output, + "required_environment_variables": required_environment_variables, + "required_secret_names": required_secret_names, + "scorer_timeout_sec": scorer_timeout_sec, + "scoring_contract": scoring_contract, + "validation_type": validation_type, + }, + scenario_update_params.ScenarioUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ScenarioView, + ) + + def list( + self, + *, + benchmark_id: str | Omit = omit, + limit: int | Omit = omit, + name: str | Omit = omit, + starting_after: str | Omit = omit, + validation_type: 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, + ) -> SyncScenariosCursorIDPage[ScenarioView]: + """ + List all Scenarios matching filter. + + Args: + benchmark_id: Filter scenarios by benchmark ID. + + limit: The limit of items to return. Default is 20. Max is 5000. + + name: Query for Scenarios with a given name. + + starting_after: Load the next page of data starting after the item with the given ID. + + validation_type: Filter by validation type + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/scenarios", + page=SyncScenariosCursorIDPage[ScenarioView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "benchmark_id": benchmark_id, + "limit": limit, + "name": name, + "starting_after": starting_after, + "validation_type": validation_type, + }, + scenario_list_params.ScenarioListParams, + ), + ), + model=ScenarioView, + ) + + def archive( + 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, + idempotency_key: str | None = None, + ) -> ScenarioView: + """Archive a previously created Scenario. + + The scenario will no longer appear in + list endpoints but can still be retrieved 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 + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/scenarios/{id}/archive", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ScenarioView, + ) + + def list_public( + self, + *, + limit: int | Omit = omit, + name: str | Omit = omit, + starting_after: 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, + ) -> SyncScenariosCursorIDPage[ScenarioView]: + """ + List all public scenarios matching filter. + + Args: + limit: The limit of items to return. Default is 20. Max is 5000. + + name: Query for Scenarios with a given name. + + starting_after: Load the next page of data starting after the item with the given ID. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/scenarios/list_public", + page=SyncScenariosCursorIDPage[ScenarioView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "name": name, + "starting_after": starting_after, + }, + scenario_list_public_params.ScenarioListPublicParams, + ), + ), + model=ScenarioView, + ) + + def start_run( + self, + *, + scenario_id: str, + benchmark_run_id: Optional[str] | Omit = omit, + metadata: Optional[Dict[str, str]] | Omit = omit, + run_name: Optional[str] | Omit = omit, + run_profile: Optional[RunProfile] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> ScenarioRunView: + """ + Start a new ScenarioRun based on the provided Scenario. + + Args: + scenario_id: ID of the Scenario to run. + + benchmark_run_id: Benchmark to associate the run. + + metadata: User defined metadata to attach to the run for organization. + + run_name: Display name of the run. + + run_profile: Runtime configuration to use for this benchmark run + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + return self._post( + "/v1/scenarios/start_run", + body=maybe_transform( + { + "scenario_id": scenario_id, + "benchmark_run_id": benchmark_run_id, + "metadata": metadata, + "run_name": run_name, + "run_profile": run_profile, + }, + scenario_start_run_params.ScenarioStartRunParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ScenarioRunView, + ) + + +class AsyncScenariosResource(AsyncAPIResource): + @cached_property + def runs(self) -> AsyncRunsResource: + return AsyncRunsResource(self._client) + + @cached_property + def scorers(self) -> AsyncScorersResource: + return AsyncScorersResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncScenariosResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return AsyncScenariosResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncScenariosResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return AsyncScenariosResourceWithStreamingResponse(self) + + async def create( + self, + *, + input_context: InputContextParam, + name: str, + scoring_contract: ScoringContractParam, + environment_parameters: Optional[ScenarioEnvironmentParam] | Omit = omit, + metadata: Optional[Dict[str, str]] | Omit = omit, + reference_output: Optional[str] | Omit = omit, + required_environment_variables: Optional[SequenceNotStr[str]] | Omit = omit, + required_secret_names: Optional[SequenceNotStr[str]] | Omit = omit, + scorer_timeout_sec: Optional[int] | Omit = omit, + validation_type: Optional[Literal["UNSPECIFIED", "FORWARD", "REVERSE", "EVALUATION"]] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> ScenarioView: + """ + Create a Scenario, a repeatable AI coding evaluation test that defines the + starting environment as well as evaluation success criteria. + + Args: + input_context: The input context for the Scenario. + + name: Name of the scenario. + + scoring_contract: The scoring contract for the Scenario. + + environment_parameters: The Environment in which the Scenario will run. + + metadata: User defined metadata to attach to the scenario for organization. + + reference_output: A string representation of the reference output to solve the scenario. Commonly + can be the result of a git diff or a sequence of command actions to apply to the + environment. + + required_environment_variables: Environment variables required to run the scenario. If these variables are not + provided, the scenario will fail to start. + + required_secret_names: Secrets required to run the scenario (user secret name to scenario required + secret name). If these secrets are not provided or the mapping is incorrect, the + scenario will fail to start. + + scorer_timeout_sec: Timeout for scoring in seconds. Default 30 minutes (1800s). + + validation_type: Validation strategy. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + return await self._post( + "/v1/scenarios", + body=await async_maybe_transform( + { + "input_context": input_context, + "name": name, + "scoring_contract": scoring_contract, + "environment_parameters": environment_parameters, + "metadata": metadata, + "reference_output": reference_output, + "required_environment_variables": required_environment_variables, + "required_secret_names": required_secret_names, + "scorer_timeout_sec": scorer_timeout_sec, + "validation_type": validation_type, + }, + scenario_create_params.ScenarioCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ScenarioView, + ) + + 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, + ) -> ScenarioView: + """ + Get a previously created scenario. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/v1/scenarios/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ScenarioView, + ) + + async def update( + self, + id: str, + *, + environment_parameters: Optional[ScenarioEnvironmentParam] | Omit = omit, + input_context: Optional[InputContextUpdateParam] | Omit = omit, + metadata: Optional[Dict[str, str]] | Omit = omit, + name: Optional[str] | Omit = omit, + reference_output: Optional[str] | Omit = omit, + required_environment_variables: Optional[SequenceNotStr[str]] | Omit = omit, + required_secret_names: Optional[SequenceNotStr[str]] | Omit = omit, + scorer_timeout_sec: Optional[int] | Omit = omit, + scoring_contract: Optional[ScoringContractUpdateParam] | Omit = omit, + validation_type: Optional[Literal["UNSPECIFIED", "FORWARD", "REVERSE", "EVALUATION"]] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> ScenarioView: + """Update a Scenario. + + Fields that are null will preserve the existing value. Fields + that are provided (including empty values) will replace the existing value + entirely. + + Args: + environment_parameters: The Environment in which the Scenario will run. + + input_context: The input context for the Scenario. + + metadata: User defined metadata to attach to the scenario. Pass in empty map to clear. + + name: Name of the scenario. Cannot be blank. + + reference_output: A string representation of the reference output to solve the scenario. Commonly + can be the result of a git diff or a sequence of command actions to apply to the + environment. Pass in empty string to clear. + + required_environment_variables: Environment variables required to run the scenario. Pass in empty list to clear. + + required_secret_names: Secrets required to run the scenario. Pass in empty list to clear. + + scorer_timeout_sec: Timeout for scoring in seconds. Default 30 minutes (1800s). + + scoring_contract: The scoring contract for the Scenario. + + validation_type: Validation strategy. Pass in empty string to clear. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/scenarios/{id}", + body=await async_maybe_transform( + { + "environment_parameters": environment_parameters, + "input_context": input_context, + "metadata": metadata, + "name": name, + "reference_output": reference_output, + "required_environment_variables": required_environment_variables, + "required_secret_names": required_secret_names, + "scorer_timeout_sec": scorer_timeout_sec, + "scoring_contract": scoring_contract, + "validation_type": validation_type, + }, + scenario_update_params.ScenarioUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ScenarioView, + ) + + def list( + self, + *, + benchmark_id: str | Omit = omit, + limit: int | Omit = omit, + name: str | Omit = omit, + starting_after: str | Omit = omit, + validation_type: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[ScenarioView, AsyncScenariosCursorIDPage[ScenarioView]]: + """ + List all Scenarios matching filter. + + Args: + benchmark_id: Filter scenarios by benchmark ID. + + limit: The limit of items to return. Default is 20. Max is 5000. + + name: Query for Scenarios with a given name. + + starting_after: Load the next page of data starting after the item with the given ID. + + validation_type: Filter by validation type + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/scenarios", + page=AsyncScenariosCursorIDPage[ScenarioView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "benchmark_id": benchmark_id, + "limit": limit, + "name": name, + "starting_after": starting_after, + "validation_type": validation_type, + }, + scenario_list_params.ScenarioListParams, + ), + ), + model=ScenarioView, + ) + + async def archive( + 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, + idempotency_key: str | None = None, + ) -> ScenarioView: + """Archive a previously created Scenario. + + The scenario will no longer appear in + list endpoints but can still be retrieved 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 + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/scenarios/{id}/archive", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ScenarioView, + ) + + def list_public( + self, + *, + limit: int | Omit = omit, + name: str | Omit = omit, + starting_after: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[ScenarioView, AsyncScenariosCursorIDPage[ScenarioView]]: + """ + List all public scenarios matching filter. + + Args: + limit: The limit of items to return. Default is 20. Max is 5000. + + name: Query for Scenarios with a given name. + + starting_after: Load the next page of data starting after the item with the given ID. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/scenarios/list_public", + page=AsyncScenariosCursorIDPage[ScenarioView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "name": name, + "starting_after": starting_after, + }, + scenario_list_public_params.ScenarioListPublicParams, + ), + ), + model=ScenarioView, + ) + + async def start_run( + self, + *, + scenario_id: str, + benchmark_run_id: Optional[str] | Omit = omit, + metadata: Optional[Dict[str, str]] | Omit = omit, + run_name: Optional[str] | Omit = omit, + run_profile: Optional[RunProfile] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> ScenarioRunView: + """ + Start a new ScenarioRun based on the provided Scenario. + + Args: + scenario_id: ID of the Scenario to run. + + benchmark_run_id: Benchmark to associate the run. + + metadata: User defined metadata to attach to the run for organization. + + run_name: Display name of the run. + + run_profile: Runtime configuration to use for this benchmark run + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + return await self._post( + "/v1/scenarios/start_run", + body=await async_maybe_transform( + { + "scenario_id": scenario_id, + "benchmark_run_id": benchmark_run_id, + "metadata": metadata, + "run_name": run_name, + "run_profile": run_profile, + }, + scenario_start_run_params.ScenarioStartRunParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ScenarioRunView, + ) + + +class ScenariosResourceWithRawResponse: + def __init__(self, scenarios: ScenariosResource) -> None: + self._scenarios = scenarios + + self.create = to_raw_response_wrapper( + scenarios.create, + ) + self.retrieve = to_raw_response_wrapper( + scenarios.retrieve, + ) + self.update = to_raw_response_wrapper( + scenarios.update, + ) + self.list = to_raw_response_wrapper( + scenarios.list, + ) + self.archive = to_raw_response_wrapper( + scenarios.archive, + ) + self.list_public = to_raw_response_wrapper( + scenarios.list_public, + ) + self.start_run = to_raw_response_wrapper( + scenarios.start_run, + ) + + @cached_property + def runs(self) -> RunsResourceWithRawResponse: + return RunsResourceWithRawResponse(self._scenarios.runs) + + @cached_property + def scorers(self) -> ScorersResourceWithRawResponse: + return ScorersResourceWithRawResponse(self._scenarios.scorers) + + +class AsyncScenariosResourceWithRawResponse: + def __init__(self, scenarios: AsyncScenariosResource) -> None: + self._scenarios = scenarios + + self.create = async_to_raw_response_wrapper( + scenarios.create, + ) + self.retrieve = async_to_raw_response_wrapper( + scenarios.retrieve, + ) + self.update = async_to_raw_response_wrapper( + scenarios.update, + ) + self.list = async_to_raw_response_wrapper( + scenarios.list, + ) + self.archive = async_to_raw_response_wrapper( + scenarios.archive, + ) + self.list_public = async_to_raw_response_wrapper( + scenarios.list_public, + ) + self.start_run = async_to_raw_response_wrapper( + scenarios.start_run, + ) + + @cached_property + def runs(self) -> AsyncRunsResourceWithRawResponse: + return AsyncRunsResourceWithRawResponse(self._scenarios.runs) + + @cached_property + def scorers(self) -> AsyncScorersResourceWithRawResponse: + return AsyncScorersResourceWithRawResponse(self._scenarios.scorers) + + +class ScenariosResourceWithStreamingResponse: + def __init__(self, scenarios: ScenariosResource) -> None: + self._scenarios = scenarios + + self.create = to_streamed_response_wrapper( + scenarios.create, + ) + self.retrieve = to_streamed_response_wrapper( + scenarios.retrieve, + ) + self.update = to_streamed_response_wrapper( + scenarios.update, + ) + self.list = to_streamed_response_wrapper( + scenarios.list, + ) + self.archive = to_streamed_response_wrapper( + scenarios.archive, + ) + self.list_public = to_streamed_response_wrapper( + scenarios.list_public, + ) + self.start_run = to_streamed_response_wrapper( + scenarios.start_run, + ) + + @cached_property + def runs(self) -> RunsResourceWithStreamingResponse: + return RunsResourceWithStreamingResponse(self._scenarios.runs) + + @cached_property + def scorers(self) -> ScorersResourceWithStreamingResponse: + return ScorersResourceWithStreamingResponse(self._scenarios.scorers) + + +class AsyncScenariosResourceWithStreamingResponse: + def __init__(self, scenarios: AsyncScenariosResource) -> None: + self._scenarios = scenarios + + self.create = async_to_streamed_response_wrapper( + scenarios.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + scenarios.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + scenarios.update, + ) + self.list = async_to_streamed_response_wrapper( + scenarios.list, + ) + self.archive = async_to_streamed_response_wrapper( + scenarios.archive, + ) + self.list_public = async_to_streamed_response_wrapper( + scenarios.list_public, + ) + self.start_run = async_to_streamed_response_wrapper( + scenarios.start_run, + ) + + @cached_property + def runs(self) -> AsyncRunsResourceWithStreamingResponse: + return AsyncRunsResourceWithStreamingResponse(self._scenarios.runs) + + @cached_property + def scorers(self) -> AsyncScorersResourceWithStreamingResponse: + return AsyncScorersResourceWithStreamingResponse(self._scenarios.scorers) diff --git a/src/runloop_api_client/resources/scenarios/scorers.py b/src/runloop_api_client/resources/scenarios/scorers.py new file mode 100644 index 000000000..cdb011dc7 --- /dev/null +++ b/src/runloop_api_client/resources/scenarios/scorers.py @@ -0,0 +1,509 @@ +# 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 ...pagination import SyncScenarioScorersCursorIDPage, AsyncScenarioScorersCursorIDPage +from ..._base_client import AsyncPaginator, make_request_options +from ...types.scenarios import scorer_list_params, scorer_create_params, scorer_update_params +from ...types.scenarios.scorer_list_response import ScorerListResponse +from ...types.scenarios.scorer_create_response import ScorerCreateResponse +from ...types.scenarios.scorer_update_response import ScorerUpdateResponse +from ...types.scenarios.scorer_retrieve_response import ScorerRetrieveResponse + +__all__ = ["ScorersResource", "AsyncScorersResource"] + + +class ScorersResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> ScorersResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return ScorersResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ScorersResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return ScorersResourceWithStreamingResponse(self) + + def create( + self, + *, + bash_script: str, + type: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> ScorerCreateResponse: + """ + Create a custom scenario scorer. + + Args: + bash_script: Bash script for the custom scorer taking context as a json object + $RL_SCORER_CONTEXT. + + type: Name of the type of custom scorer. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + return self._post( + "/v1/scenarios/scorers", + body=maybe_transform( + { + "bash_script": bash_script, + "type": type, + }, + scorer_create_params.ScorerCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ScorerCreateResponse, + ) + + 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, + ) -> ScorerRetrieveResponse: + """ + Retrieve Scenario Scorer. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/v1/scenarios/scorers/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ScorerRetrieveResponse, + ) + + def update( + self, + id: str, + *, + bash_script: str, + type: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> ScorerUpdateResponse: + """ + Update a scenario scorer. + + Args: + bash_script: Bash script for the custom scorer taking context as a json object + $RL_SCORER_CONTEXT. + + type: Name of the type of custom scorer. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/scenarios/scorers/{id}", + body=maybe_transform( + { + "bash_script": bash_script, + "type": type, + }, + scorer_update_params.ScorerUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ScorerUpdateResponse, + ) + + def list( + self, + *, + limit: int | Omit = omit, + starting_after: 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, + ) -> SyncScenarioScorersCursorIDPage[ScorerListResponse]: + """ + List all Scenario Scorers matching filter. + + Args: + limit: The limit of items to return. Default is 20. Max is 5000. + + starting_after: Load the next page of data starting after the item with the given ID. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/scenarios/scorers", + page=SyncScenarioScorersCursorIDPage[ScorerListResponse], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "starting_after": starting_after, + }, + scorer_list_params.ScorerListParams, + ), + ), + model=ScorerListResponse, + ) + + +class AsyncScorersResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncScorersResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return AsyncScorersResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncScorersResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return AsyncScorersResourceWithStreamingResponse(self) + + async def create( + self, + *, + bash_script: str, + type: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> ScorerCreateResponse: + """ + Create a custom scenario scorer. + + Args: + bash_script: Bash script for the custom scorer taking context as a json object + $RL_SCORER_CONTEXT. + + type: Name of the type of custom scorer. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + return await self._post( + "/v1/scenarios/scorers", + body=await async_maybe_transform( + { + "bash_script": bash_script, + "type": type, + }, + scorer_create_params.ScorerCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ScorerCreateResponse, + ) + + 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, + ) -> ScorerRetrieveResponse: + """ + Retrieve Scenario Scorer. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/v1/scenarios/scorers/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ScorerRetrieveResponse, + ) + + async def update( + self, + id: str, + *, + bash_script: str, + type: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> ScorerUpdateResponse: + """ + Update a scenario scorer. + + Args: + bash_script: Bash script for the custom scorer taking context as a json object + $RL_SCORER_CONTEXT. + + type: Name of the type of custom scorer. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/scenarios/scorers/{id}", + body=await async_maybe_transform( + { + "bash_script": bash_script, + "type": type, + }, + scorer_update_params.ScorerUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ScorerUpdateResponse, + ) + + def list( + self, + *, + limit: int | Omit = omit, + starting_after: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[ScorerListResponse, AsyncScenarioScorersCursorIDPage[ScorerListResponse]]: + """ + List all Scenario Scorers matching filter. + + Args: + limit: The limit of items to return. Default is 20. Max is 5000. + + starting_after: Load the next page of data starting after the item with the given ID. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/scenarios/scorers", + page=AsyncScenarioScorersCursorIDPage[ScorerListResponse], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "starting_after": starting_after, + }, + scorer_list_params.ScorerListParams, + ), + ), + model=ScorerListResponse, + ) + + +class ScorersResourceWithRawResponse: + def __init__(self, scorers: ScorersResource) -> None: + self._scorers = scorers + + self.create = to_raw_response_wrapper( + scorers.create, + ) + self.retrieve = to_raw_response_wrapper( + scorers.retrieve, + ) + self.update = to_raw_response_wrapper( + scorers.update, + ) + self.list = to_raw_response_wrapper( + scorers.list, + ) + + +class AsyncScorersResourceWithRawResponse: + def __init__(self, scorers: AsyncScorersResource) -> None: + self._scorers = scorers + + self.create = async_to_raw_response_wrapper( + scorers.create, + ) + self.retrieve = async_to_raw_response_wrapper( + scorers.retrieve, + ) + self.update = async_to_raw_response_wrapper( + scorers.update, + ) + self.list = async_to_raw_response_wrapper( + scorers.list, + ) + + +class ScorersResourceWithStreamingResponse: + def __init__(self, scorers: ScorersResource) -> None: + self._scorers = scorers + + self.create = to_streamed_response_wrapper( + scorers.create, + ) + self.retrieve = to_streamed_response_wrapper( + scorers.retrieve, + ) + self.update = to_streamed_response_wrapper( + scorers.update, + ) + self.list = to_streamed_response_wrapper( + scorers.list, + ) + + +class AsyncScorersResourceWithStreamingResponse: + def __init__(self, scorers: AsyncScorersResource) -> None: + self._scorers = scorers + + self.create = async_to_streamed_response_wrapper( + scorers.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + scorers.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + scorers.update, + ) + self.list = async_to_streamed_response_wrapper( + scorers.list, + ) diff --git a/src/runloop_api_client/resources/secrets.py b/src/runloop_api_client/resources/secrets.py new file mode 100644 index 000000000..892557497 --- /dev/null +++ b/src/runloop_api_client/resources/secrets.py @@ -0,0 +1,500 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..types import secret_list_params, secret_create_params, secret_update_params +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.secret_view import SecretView +from ..types.secret_list_view import SecretListView + +__all__ = ["SecretsResource", "AsyncSecretsResource"] + + +class SecretsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> SecretsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return SecretsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> SecretsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return SecretsResourceWithStreamingResponse(self) + + def create( + self, + *, + name: str, + value: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> SecretView: + """Create a new Secret with a globally unique name and value. + + The Secret will be + encrypted at rest and made available as an environment variable in Devboxes. + + Args: + name: The globally unique name for the Secret. Must be a valid environment variable + name (alphanumeric and underscores only). Example: 'DATABASE_PASSWORD' + + value: The value to store for this Secret. This will be encrypted at rest and made + available as an environment variable in Devboxes. Example: 'my-secure-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 + + idempotency_key: Specify a custom idempotency key for this request + """ + return self._post( + "/v1/secrets", + body=maybe_transform( + { + "name": name, + "value": value, + }, + secret_create_params.SecretCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=SecretView, + ) + + def update( + self, + name: str, + *, + value: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> SecretView: + """Update the value of an existing Secret by name. + + The new value will be encrypted + at rest. + + Args: + value: The new value for the Secret. This will replace the existing value and be + encrypted at rest. Example: 'my-updated-secure-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 + + idempotency_key: Specify a custom idempotency key for this request + """ + if not name: + raise ValueError(f"Expected a non-empty value for `name` but received {name!r}") + return self._post( + f"/v1/secrets/{name}", + body=maybe_transform({"value": value}, secret_update_params.SecretUpdateParams), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=SecretView, + ) + + def list( + self, + *, + limit: 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, + ) -> SecretListView: + """List all Secrets for the authenticated account. + + Secret values are not included + for security reasons. + + Args: + limit: The limit of items to return. Default is 20. Max is 5000. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get( + "/v1/secrets", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform({"limit": limit}, secret_list_params.SecretListParams), + ), + cast_to=SecretListView, + ) + + def delete( + self, + name: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> SecretView: + """Delete an existing Secret by name. + + This action is irreversible and will remove + the Secret from all Devboxes. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not name: + raise ValueError(f"Expected a non-empty value for `name` but received {name!r}") + return self._post( + f"/v1/secrets/{name}/delete", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=SecretView, + ) + + +class AsyncSecretsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncSecretsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return AsyncSecretsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncSecretsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return AsyncSecretsResourceWithStreamingResponse(self) + + async def create( + self, + *, + name: str, + value: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> SecretView: + """Create a new Secret with a globally unique name and value. + + The Secret will be + encrypted at rest and made available as an environment variable in Devboxes. + + Args: + name: The globally unique name for the Secret. Must be a valid environment variable + name (alphanumeric and underscores only). Example: 'DATABASE_PASSWORD' + + value: The value to store for this Secret. This will be encrypted at rest and made + available as an environment variable in Devboxes. Example: 'my-secure-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 + + idempotency_key: Specify a custom idempotency key for this request + """ + return await self._post( + "/v1/secrets", + body=await async_maybe_transform( + { + "name": name, + "value": value, + }, + secret_create_params.SecretCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=SecretView, + ) + + async def update( + self, + name: str, + *, + value: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> SecretView: + """Update the value of an existing Secret by name. + + The new value will be encrypted + at rest. + + Args: + value: The new value for the Secret. This will replace the existing value and be + encrypted at rest. Example: 'my-updated-secure-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 + + idempotency_key: Specify a custom idempotency key for this request + """ + if not name: + raise ValueError(f"Expected a non-empty value for `name` but received {name!r}") + return await self._post( + f"/v1/secrets/{name}", + body=await async_maybe_transform({"value": value}, secret_update_params.SecretUpdateParams), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=SecretView, + ) + + async def list( + self, + *, + limit: 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, + ) -> SecretListView: + """List all Secrets for the authenticated account. + + Secret values are not included + for security reasons. + + Args: + limit: The limit of items to return. Default is 20. Max is 5000. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._get( + "/v1/secrets", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform({"limit": limit}, secret_list_params.SecretListParams), + ), + cast_to=SecretListView, + ) + + async def delete( + self, + name: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> SecretView: + """Delete an existing Secret by name. + + This action is irreversible and will remove + the Secret from all Devboxes. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not name: + raise ValueError(f"Expected a non-empty value for `name` but received {name!r}") + return await self._post( + f"/v1/secrets/{name}/delete", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=SecretView, + ) + + +class SecretsResourceWithRawResponse: + def __init__(self, secrets: SecretsResource) -> None: + self._secrets = secrets + + self.create = to_raw_response_wrapper( + secrets.create, + ) + self.update = to_raw_response_wrapper( + secrets.update, + ) + self.list = to_raw_response_wrapper( + secrets.list, + ) + self.delete = to_raw_response_wrapper( + secrets.delete, + ) + + +class AsyncSecretsResourceWithRawResponse: + def __init__(self, secrets: AsyncSecretsResource) -> None: + self._secrets = secrets + + self.create = async_to_raw_response_wrapper( + secrets.create, + ) + self.update = async_to_raw_response_wrapper( + secrets.update, + ) + self.list = async_to_raw_response_wrapper( + secrets.list, + ) + self.delete = async_to_raw_response_wrapper( + secrets.delete, + ) + + +class SecretsResourceWithStreamingResponse: + def __init__(self, secrets: SecretsResource) -> None: + self._secrets = secrets + + self.create = to_streamed_response_wrapper( + secrets.create, + ) + self.update = to_streamed_response_wrapper( + secrets.update, + ) + self.list = to_streamed_response_wrapper( + secrets.list, + ) + self.delete = to_streamed_response_wrapper( + secrets.delete, + ) + + +class AsyncSecretsResourceWithStreamingResponse: + def __init__(self, secrets: AsyncSecretsResource) -> None: + self._secrets = secrets + + self.create = async_to_streamed_response_wrapper( + secrets.create, + ) + self.update = async_to_streamed_response_wrapper( + secrets.update, + ) + self.list = async_to_streamed_response_wrapper( + secrets.list, + ) + self.delete = async_to_streamed_response_wrapper( + secrets.delete, + ) diff --git a/src/runloop_api_client/types/__init__.py b/src/runloop_api_client/types/__init__.py new file mode 100644 index 000000000..2cd10b43d --- /dev/null +++ b/src/runloop_api_client/types/__init__.py @@ -0,0 +1,137 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .shared import ( + Mount as Mount, + AfterIdle as AfterIdle, + AgentMount as AgentMount, + RunProfile as RunProfile, + AgentSource as AgentSource, + ObjectMount as ObjectMount, + LaunchParameters as LaunchParameters, + CodeMountParameters as CodeMountParameters, +) +from .agent_view import AgentView as AgentView +from .devbox_view import DevboxView as DevboxView +from .object_view import ObjectView as ObjectView +from .secret_view import SecretView as SecretView +from .tunnel_view import TunnelView as TunnelView +from .input_context import InputContext as InputContext +from .scenario_view import ScenarioView as ScenarioView +from .benchmark_view import BenchmarkView as BenchmarkView +from .blueprint_view import BlueprintView as BlueprintView +from .agent_list_view import AgentListView as AgentListView +from .mcp_config_view import McpConfigView as McpConfigView +from .devbox_list_view import DevboxListView as DevboxListView +from .object_list_view import ObjectListView as ObjectListView +from .scoring_contract import ScoringContract as ScoringContract +from .scoring_function import ScoringFunction as ScoringFunction +from .secret_list_view import SecretListView as SecretListView +from .agent_list_params import AgentListParams as AgentListParams +from .scenario_run_view import ScenarioRunView as ScenarioRunView +from .benchmark_job_view import BenchmarkJobView as BenchmarkJobView +from .benchmark_run_view import BenchmarkRunView as BenchmarkRunView +from .devbox_list_params import DevboxListParams as DevboxListParams +from .devbox_tunnel_view import DevboxTunnelView as DevboxTunnelView +from .object_list_params import ObjectListParams as ObjectListParams +from .secret_list_params import SecretListParams as SecretListParams +from .agent_create_params import AgentCreateParams as AgentCreateParams +from .blueprint_build_log import BlueprintBuildLog as BlueprintBuildLog +from .blueprint_list_view import BlueprintListView as BlueprintListView +from .gateway_config_view import GatewayConfigView as GatewayConfigView +from .input_context_param import InputContextParam as InputContextParam +from .network_policy_view import NetworkPolicyView as NetworkPolicyView +from .devbox_create_params import DevboxCreateParams as DevboxCreateParams +from .devbox_snapshot_view import DevboxSnapshotView as DevboxSnapshotView +from .devbox_update_params import DevboxUpdateParams as DevboxUpdateParams +from .mcp_config_list_view import McpConfigListView as McpConfigListView +from .object_create_params import ObjectCreateParams as ObjectCreateParams +from .scenario_environment import ScenarioEnvironment as ScenarioEnvironment +from .scenario_list_params import ScenarioListParams as ScenarioListParams +from .secret_create_params import SecretCreateParams as SecretCreateParams +from .secret_update_params import SecretUpdateParams as SecretUpdateParams +from .benchmark_list_params import BenchmarkListParams as BenchmarkListParams +from .blueprint_list_params import BlueprintListParams as BlueprintListParams +from .devbox_execute_params import DevboxExecuteParams as DevboxExecuteParams +from .blueprint_preview_view import BlueprintPreviewView as BlueprintPreviewView +from .devbox_shutdown_params import DevboxShutdownParams as DevboxShutdownParams +from .mcp_config_list_params import McpConfigListParams as McpConfigListParams +from .object_download_params import ObjectDownloadParams as ObjectDownloadParams +from .repository_list_params import RepositoryListParams as RepositoryListParams +from .scenario_create_params import ScenarioCreateParams as ScenarioCreateParams +from .scenario_run_list_view import ScenarioRunListView as ScenarioRunListView +from .scenario_update_params import ScenarioUpdateParams as ScenarioUpdateParams +from .scoring_contract_param import ScoringContractParam as ScoringContractParam +from .scoring_function_param import ScoringFunctionParam as ScoringFunctionParam +from .benchmark_create_params import BenchmarkCreateParams as BenchmarkCreateParams +from .benchmark_job_list_view import BenchmarkJobListView as BenchmarkJobListView +from .benchmark_run_list_view import BenchmarkRunListView as BenchmarkRunListView +from .benchmark_update_params import BenchmarkUpdateParams as BenchmarkUpdateParams +from .blueprint_create_params import BlueprintCreateParams as BlueprintCreateParams +from .inspection_source_param import InspectionSourceParam as InspectionSourceParam +from .blueprint_preview_params import BlueprintPreviewParams as BlueprintPreviewParams +from .gateway_config_list_view import GatewayConfigListView as GatewayConfigListView +from .mcp_config_create_params import McpConfigCreateParams as McpConfigCreateParams +from .mcp_config_update_params import McpConfigUpdateParams as McpConfigUpdateParams +from .network_policy_list_view import NetworkPolicyListView as NetworkPolicyListView +from .object_download_url_view import ObjectDownloadURLView as ObjectDownloadURLView +from .repository_create_params import RepositoryCreateParams as RepositoryCreateParams +from .repository_manifest_view import RepositoryManifestView as RepositoryManifestView +from .benchmark_job_list_params import BenchmarkJobListParams as BenchmarkJobListParams +from .benchmark_run_list_params import BenchmarkRunListParams as BenchmarkRunListParams +from .devbox_send_std_in_result import DevboxSendStdInResult as DevboxSendStdInResult +from .devbox_snapshot_list_view import DevboxSnapshotListView as DevboxSnapshotListView +from .devbox_upload_file_params import DevboxUploadFileParams as DevboxUploadFileParams +from .object_list_public_params import ObjectListPublicParams as ObjectListPublicParams +from .repository_inspect_params import RepositoryInspectParams as RepositoryInspectParams +from .repository_refresh_params import RepositoryRefreshParams as RepositoryRefreshParams +from .scenario_start_run_params import ScenarioStartRunParams as ScenarioStartRunParams +from .benchmark_start_run_params import BenchmarkStartRunParams as BenchmarkStartRunParams +from .blueprint_build_parameters import BlueprintBuildParameters as BlueprintBuildParameters +from .devbox_execute_sync_params import DevboxExecuteSyncParams as DevboxExecuteSyncParams +from .devbox_resource_usage_view import DevboxResourceUsageView as DevboxResourceUsageView +from .gateway_config_list_params import GatewayConfigListParams as GatewayConfigListParams +from .input_context_update_param import InputContextUpdateParam as InputContextUpdateParam +from .network_policy_list_params import NetworkPolicyListParams as NetworkPolicyListParams +from .repository_connection_view import RepositoryConnectionView as RepositoryConnectionView +from .scenario_environment_param import ScenarioEnvironmentParam as ScenarioEnvironmentParam +from .benchmark_job_create_params import BenchmarkJobCreateParams as BenchmarkJobCreateParams +from .devbox_create_tunnel_params import DevboxCreateTunnelParams as DevboxCreateTunnelParams +from .devbox_download_file_params import DevboxDownloadFileParams as DevboxDownloadFileParams +from .devbox_enable_tunnel_params import DevboxEnableTunnelParams as DevboxEnableTunnelParams +from .devbox_execute_async_params import DevboxExecuteAsyncParams as DevboxExecuteAsyncParams +from .devbox_remove_tunnel_params import DevboxRemoveTunnelParams as DevboxRemoveTunnelParams +from .devbox_snapshot_disk_params import DevboxSnapshotDiskParams as DevboxSnapshotDiskParams +from .scenario_list_public_params import ScenarioListPublicParams as ScenarioListPublicParams +from .benchmark_definitions_params import BenchmarkDefinitionsParams as BenchmarkDefinitionsParams +from .benchmark_list_public_params import BenchmarkListPublicParams as BenchmarkListPublicParams +from .blueprint_list_public_params import BlueprintListPublicParams as BlueprintListPublicParams +from .devbox_execution_detail_view import DevboxExecutionDetailView as DevboxExecutionDetailView +from .gateway_config_create_params import GatewayConfigCreateParams as GatewayConfigCreateParams +from .gateway_config_update_params import GatewayConfigUpdateParams as GatewayConfigUpdateParams +from .network_policy_create_params import NetworkPolicyCreateParams as NetworkPolicyCreateParams +from .network_policy_update_params import NetworkPolicyUpdateParams as NetworkPolicyUpdateParams +from .scoring_contract_result_view import ScoringContractResultView as ScoringContractResultView +from .scoring_function_result_view import ScoringFunctionResultView as ScoringFunctionResultView +from .repository_inspection_details import RepositoryInspectionDetails as RepositoryInspectionDetails +from .scenario_definition_list_view import ScenarioDefinitionListView as ScenarioDefinitionListView +from .scoring_contract_update_param import ScoringContractUpdateParam as ScoringContractUpdateParam +from .blueprint_build_logs_list_view import BlueprintBuildLogsListView as BlueprintBuildLogsListView +from .devbox_create_ssh_key_response import DevboxCreateSSHKeyResponse as DevboxCreateSSHKeyResponse +from .devbox_wait_for_command_params import DevboxWaitForCommandParams as DevboxWaitForCommandParams +from .repository_connection_list_view import RepositoryConnectionListView as RepositoryConnectionListView +from .repository_inspection_list_view import RepositoryInspectionListView as RepositoryInspectionListView +from .devbox_read_file_contents_params import DevboxReadFileContentsParams as DevboxReadFileContentsParams +from .benchmark_update_scenarios_params import BenchmarkUpdateScenariosParams as BenchmarkUpdateScenariosParams +from .devbox_list_disk_snapshots_params import DevboxListDiskSnapshotsParams as DevboxListDiskSnapshotsParams +from .devbox_snapshot_disk_async_params import DevboxSnapshotDiskAsyncParams as DevboxSnapshotDiskAsyncParams +from .devbox_write_file_contents_params import DevboxWriteFileContentsParams as DevboxWriteFileContentsParams +from .devbox_async_execution_detail_view import DevboxAsyncExecutionDetailView as DevboxAsyncExecutionDetailView +from .devbox_read_file_contents_response import DevboxReadFileContentsResponse as DevboxReadFileContentsResponse +from .benchmark_run_list_scenario_runs_params import ( + BenchmarkRunListScenarioRunsParams as BenchmarkRunListScenarioRunsParams, +) +from .blueprint_create_from_inspection_params import ( + BlueprintCreateFromInspectionParams as BlueprintCreateFromInspectionParams, +) diff --git a/src/runloop_api_client/types/agent_create_params.py b/src/runloop_api_client/types/agent_create_params.py new file mode 100644 index 000000000..3c2deff2a --- /dev/null +++ b/src/runloop_api_client/types/agent_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 import Optional +from typing_extensions import Required, TypedDict + +from .shared_params.agent_source import AgentSource + +__all__ = ["AgentCreateParams"] + + +class AgentCreateParams(TypedDict, total=False): + name: Required[str] + """The name of the Agent.""" + + version: Required[str] + """The version of the Agent. Must be a semver string (e.g., '2.0.65') or a SHA.""" + + source: Optional[AgentSource] + """The source configuration for the Agent.""" diff --git a/src/runloop_api_client/types/agent_list_params.py b/src/runloop_api_client/types/agent_list_params.py new file mode 100644 index 000000000..3df89fc25 --- /dev/null +++ b/src/runloop_api_client/types/agent_list_params.py @@ -0,0 +1,27 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["AgentListParams"] + + +class AgentListParams(TypedDict, total=False): + is_public: bool + """Filter agents by public visibility.""" + + limit: int + """The limit of items to return. Default is 20. Max is 5000.""" + + name: str + """Filter agents by name (partial match supported).""" + + search: str + """Search by agent ID or name.""" + + starting_after: str + """Load the next page of data starting after the item with the given ID.""" + + version: str + """Filter by version. Use 'latest' to get the most recently created agent.""" diff --git a/src/runloop_api_client/types/agent_list_view.py b/src/runloop_api_client/types/agent_list_view.py new file mode 100644 index 000000000..9e57a9769 --- /dev/null +++ b/src/runloop_api_client/types/agent_list_view.py @@ -0,0 +1,30 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel +from .agent_view import AgentView + +__all__ = ["AgentListView"] + + +class AgentListView(BaseModel): + """A paginated list of Agents.""" + + agents: List[AgentView] + """The list of Agents.""" + + has_more: bool + """Whether there are more Agents to fetch.""" + + remaining_count: Optional[int] = None + """The count of remaining Agents. + + Deprecated: will be removed in a future breaking change. + """ + + total_count: Optional[int] = None + """The total count of Agents. + + Deprecated: will be removed in a future breaking change. + """ diff --git a/src/runloop_api_client/types/agent_view.py b/src/runloop_api_client/types/agent_view.py new file mode 100644 index 000000000..23b1f68ff --- /dev/null +++ b/src/runloop_api_client/types/agent_view.py @@ -0,0 +1,30 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel +from .shared.agent_source import AgentSource + +__all__ = ["AgentView"] + + +class AgentView(BaseModel): + """An Agent represents a registered AI agent entity.""" + + id: str + """The unique identifier of the Agent.""" + + create_time_ms: int + """The creation time of the Agent (Unix timestamp milliseconds).""" + + is_public: bool + """Whether the Agent is publicly accessible.""" + + name: str + """The name of the Agent.""" + + version: str + """The version of the Agent. A semver string (e.g., '2.0.65') or a SHA.""" + + source: Optional[AgentSource] = None + """The source configuration for the Agent.""" diff --git a/src/runloop_api_client/types/benchmark_create_params.py b/src/runloop_api_client/types/benchmark_create_params.py new file mode 100644 index 000000000..36f7b95a9 --- /dev/null +++ b/src/runloop_api_client/types/benchmark_create_params.py @@ -0,0 +1,40 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, Optional +from typing_extensions import Required, TypedDict + +from .._types import SequenceNotStr + +__all__ = ["BenchmarkCreateParams"] + + +class BenchmarkCreateParams(TypedDict, total=False): + name: Required[str] + """The unique name of the Benchmark.""" + + attribution: Optional[str] + """Attribution information for the benchmark.""" + + description: Optional[str] + """Detailed description of the benchmark.""" + + metadata: Optional[Dict[str, str]] + """User defined metadata to attach to the benchmark.""" + + required_environment_variables: Optional[SequenceNotStr[str]] + """Environment variables required to run the benchmark. + + If any required variables are not supplied, the benchmark will fail to start. + """ + + required_secret_names: SequenceNotStr[str] + """ + Secrets required to run the benchmark with (environment variable name will be + mapped to the your user secret by name). If any of these secrets are not + provided or the mapping is incorrect, the benchmark will fail to start. + """ + + scenario_ids: Optional[SequenceNotStr[str]] + """The Scenario IDs that make up the Benchmark.""" diff --git a/src/runloop_api_client/types/benchmark_definitions_params.py b/src/runloop_api_client/types/benchmark_definitions_params.py new file mode 100644 index 000000000..97caff125 --- /dev/null +++ b/src/runloop_api_client/types/benchmark_definitions_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__ = ["BenchmarkDefinitionsParams"] + + +class BenchmarkDefinitionsParams(TypedDict, total=False): + limit: int + """The limit of items to return. Default is 20. Max is 5000.""" + + starting_after: str + """Load the next page of data starting after the item with the given ID.""" diff --git a/src/runloop_api_client/types/benchmark_job_create_params.py b/src/runloop_api_client/types/benchmark_job_create_params.py new file mode 100644 index 000000000..8ac3a5475 --- /dev/null +++ b/src/runloop_api_client/types/benchmark_job_create_params.py @@ -0,0 +1,220 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, Union, Iterable, Optional +from typing_extensions import Literal, Required, TypeAlias, TypedDict + +from .._types import SequenceNotStr + +__all__ = [ + "BenchmarkJobCreateParams", + "Spec", + "SpecHarborJobSpec", + "SpecBenchmarkDefinitionJobSpec", + "SpecBenchmarkDefinitionJobSpecAgentConfig", + "SpecBenchmarkDefinitionJobSpecAgentConfigAgentEnvironment", + "SpecBenchmarkDefinitionJobSpecOrchestratorConfig", + "SpecScenarioDefinitionJobSpec", + "SpecScenarioDefinitionJobSpecAgentConfig", + "SpecScenarioDefinitionJobSpecAgentConfigAgentEnvironment", + "SpecScenarioDefinitionJobSpecOrchestratorConfig", +] + + +class BenchmarkJobCreateParams(TypedDict, total=False): + name: Optional[str] + """The name of the BenchmarkJob. + + If not provided, name will be generated based on target dataset. + """ + + spec: Optional[Spec] + """The job specification. Exactly one spec type must be set.""" + + +class SpecHarborJobSpec(TypedDict, total=False): + """Harbor-based job specification with inline YAML configuration.""" + + inline_yaml: Required[str] + """The Harbor job configuration as inline YAML content.""" + + type: Required[Literal["harbor"]] + + +class SpecBenchmarkDefinitionJobSpecAgentConfigAgentEnvironment(TypedDict, total=False): + """Environment configuration to use for this agent""" + + environment_variables: Optional[Dict[str, str]] + """Environment variables to set when launching the agent.""" + + secrets: Optional[Dict[str, str]] + """Secrets to inject as environment variables when launching the agent. + + Map of environment variable names to secret IDs. + """ + + +class SpecBenchmarkDefinitionJobSpecAgentConfig(TypedDict, total=False): + """Configuration for an agent in a benchmark job""" + + name: Required[str] + """Name of the agent""" + + type: Required[Literal["job_agent"]] + + agent_environment: Optional[SpecBenchmarkDefinitionJobSpecAgentConfigAgentEnvironment] + """Environment configuration to use for this agent""" + + agent_id: Optional[str] + """ID of the agent to use (optional if agent exists by name)""" + + kwargs: Optional[Dict[str, str]] + """Additional kwargs for agent configuration""" + + model_name: Optional[str] + """Model name override for this agent""" + + timeout_seconds: Optional[float] + """Timeout in seconds for this agent""" + + +class SpecBenchmarkDefinitionJobSpecOrchestratorConfig(TypedDict, total=False): + """Orchestrator configuration (optional overrides). + + If not provided, default values will be used. + """ + + n_attempts: Optional[int] + """Number of retry attempts on failure (default: 0). + + This is the retry policy for failed scenarios. Default is 0. + """ + + n_concurrent_trials: Optional[int] + """Number of concurrent trials to run (default: 1). + + Controls parallelism for scenario execution. Default is 1. + """ + + quiet: Optional[bool] + """Suppress verbose output (default: false)""" + + timeout_multiplier: Optional[float] + """Timeout multiplier for retries (default: 1.0). + + Each retry will multiply the timeout by this factor. + """ + + +class SpecBenchmarkDefinitionJobSpec(TypedDict, total=False): + """Specifies a benchmark definition with runtime configuration. + + The benchmark definition's scenarios will be executed using the provided agent and orchestrator configurations. + """ + + agent_configs: Required[Iterable[SpecBenchmarkDefinitionJobSpecAgentConfig]] + """Agent configurations to use for this run. Must specify at least one agent.""" + + benchmark_id: Required[str] + """ID of the benchmark definition to run. + + The scenarios from this benchmark will be executed. + """ + + type: Required[Literal["benchmark"]] + + orchestrator_config: Optional[SpecBenchmarkDefinitionJobSpecOrchestratorConfig] + """Orchestrator configuration (optional overrides). + + If not provided, default values will be used. + """ + + +class SpecScenarioDefinitionJobSpecAgentConfigAgentEnvironment(TypedDict, total=False): + """Environment configuration to use for this agent""" + + environment_variables: Optional[Dict[str, str]] + """Environment variables to set when launching the agent.""" + + secrets: Optional[Dict[str, str]] + """Secrets to inject as environment variables when launching the agent. + + Map of environment variable names to secret IDs. + """ + + +class SpecScenarioDefinitionJobSpecAgentConfig(TypedDict, total=False): + """Configuration for an agent in a benchmark job""" + + name: Required[str] + """Name of the agent""" + + type: Required[Literal["job_agent"]] + + agent_environment: Optional[SpecScenarioDefinitionJobSpecAgentConfigAgentEnvironment] + """Environment configuration to use for this agent""" + + agent_id: Optional[str] + """ID of the agent to use (optional if agent exists by name)""" + + kwargs: Optional[Dict[str, str]] + """Additional kwargs for agent configuration""" + + model_name: Optional[str] + """Model name override for this agent""" + + timeout_seconds: Optional[float] + """Timeout in seconds for this agent""" + + +class SpecScenarioDefinitionJobSpecOrchestratorConfig(TypedDict, total=False): + """Orchestrator configuration (optional overrides). + + If not provided, default values will be used. + """ + + n_attempts: Optional[int] + """Number of retry attempts on failure (default: 0). + + This is the retry policy for failed scenarios. Default is 0. + """ + + n_concurrent_trials: Optional[int] + """Number of concurrent trials to run (default: 1). + + Controls parallelism for scenario execution. Default is 1. + """ + + quiet: Optional[bool] + """Suppress verbose output (default: false)""" + + timeout_multiplier: Optional[float] + """Timeout multiplier for retries (default: 1.0). + + Each retry will multiply the timeout by this factor. + """ + + +class SpecScenarioDefinitionJobSpec(TypedDict, total=False): + """Specifies a set of scenarios with runtime configuration. + + The scenarios will be executed using the provided agent and orchestrator configurations. + """ + + agent_configs: Required[Iterable[SpecScenarioDefinitionJobSpecAgentConfig]] + """Agent configurations to use for this run. Must specify at least one agent.""" + + scenario_ids: Required[SequenceNotStr[str]] + """List of scenario IDs to execute""" + + type: Required[Literal["scenarios"]] + + orchestrator_config: Optional[SpecScenarioDefinitionJobSpecOrchestratorConfig] + """Orchestrator configuration (optional overrides). + + If not provided, default values will be used. + """ + + +Spec: TypeAlias = Union[SpecHarborJobSpec, SpecBenchmarkDefinitionJobSpec, SpecScenarioDefinitionJobSpec] diff --git a/src/runloop_api_client/types/benchmark_job_list_params.py b/src/runloop_api_client/types/benchmark_job_list_params.py new file mode 100644 index 000000000..c0db8843c --- /dev/null +++ b/src/runloop_api_client/types/benchmark_job_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__ = ["BenchmarkJobListParams"] + + +class BenchmarkJobListParams(TypedDict, total=False): + limit: int + """The limit of items to return. Default is 20. Max is 5000.""" + + name: str + """Filter by name""" + + starting_after: str + """Load the next page of data starting after the item with the given ID.""" diff --git a/src/runloop_api_client/types/benchmark_job_list_view.py b/src/runloop_api_client/types/benchmark_job_list_view.py new file mode 100644 index 000000000..f0e1da7d0 --- /dev/null +++ b/src/runloop_api_client/types/benchmark_job_list_view.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel +from .benchmark_job_view import BenchmarkJobView + +__all__ = ["BenchmarkJobListView"] + + +class BenchmarkJobListView(BaseModel): + has_more: bool + + jobs: List[BenchmarkJobView] + """List of BenchmarkJobs matching filter.""" + + remaining_count: Optional[int] = None + + total_count: Optional[int] = None diff --git a/src/runloop_api_client/types/benchmark_job_view.py b/src/runloop_api_client/types/benchmark_job_view.py new file mode 100644 index 000000000..f245f33ac --- /dev/null +++ b/src/runloop_api_client/types/benchmark_job_view.py @@ -0,0 +1,344 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, List, Union, Optional +from typing_extensions import Literal, Annotated, TypeAlias + +from pydantic import Field as FieldInfo + +from .._utils import PropertyInfo +from .._models import BaseModel + +__all__ = [ + "BenchmarkJobView", + "BenchmarkOutcome", + "BenchmarkOutcomeScenarioOutcome", + "BenchmarkOutcomeScenarioOutcomeFailureReason", + "InProgressRun", + "InProgressRunAgentConfig", + "InProgressRunAgentConfigExternalAPIAgentConfig", + "InProgressRunAgentConfigJobAgentConfig", + "InProgressRunAgentConfigJobAgentConfigAgentEnvironment", + "JobSource", + "JobSourceHarborJobSource", + "JobSourceBenchmarkDefJobSource", + "JobSourceScenariosJobSource", + "JobSpec", + "JobSpecAgentConfig", + "JobSpecAgentConfigAgentEnvironment", + "JobSpecOrchestratorConfig", +] + + +class BenchmarkOutcomeScenarioOutcomeFailureReason(BaseModel): + """Failure information if the scenario failed or timed out. + + Contains exception type and message. + """ + + exception_message: str + """The exception message providing context""" + + exception_type: str + """The exception class name (e.g., 'TimeoutException', 'AgentTimeoutError')""" + + +class BenchmarkOutcomeScenarioOutcome(BaseModel): + """ + Outcome data for a single scenario execution, including its final state and scoring results. + """ + + scenario_definition_id: str + """The ID of the scenario definition that was executed.""" + + scenario_name: str + """The name of the scenario.""" + + scenario_run_id: str + """The ID of the scenario run.""" + + state: Literal["COMPLETED", "FAILED", "TIMEOUT", "CANCELED"] + """The final state of the scenario execution.""" + + duration_ms: Optional[int] = None + """Duration of the scenario execution in milliseconds.""" + + failure_reason: Optional[BenchmarkOutcomeScenarioOutcomeFailureReason] = None + """Failure information if the scenario failed or timed out. + + Contains exception type and message. + """ + + score: Optional[float] = None + """The score achieved for this scenario (0.0 to 1.0). + + Only present if state is COMPLETED. + """ + + +class BenchmarkOutcome(BaseModel): + """ + Outcome data for a single benchmark run within a benchmark job, representing results for one agent configuration. + """ + + agent_name: str + """The name of the agent configuration used.""" + + benchmark_run_id: str + """The ID of the benchmark run.""" + + n_completed: int + """Number of scenarios that completed successfully.""" + + n_failed: int + """Number of scenarios that failed.""" + + n_timeout: int + """Number of scenarios that timed out.""" + + scenario_outcomes: List[BenchmarkOutcomeScenarioOutcome] + """Detailed outcomes for each scenario in this benchmark run.""" + + average_score: Optional[float] = None + """Average score across all completed scenarios (0.0 to 1.0).""" + + duration_ms: Optional[int] = None + """Total duration of the benchmark run in milliseconds.""" + + api_model_name: Optional[str] = FieldInfo(alias="model_name", default=None) + """The model name used by the agent.""" + + +class InProgressRunAgentConfigExternalAPIAgentConfig(BaseModel): + """Configuration for externally-driven benchmark runs via API""" + + type: Literal["external_api"] + + info: Optional[str] = None + """Placeholder for future external agent metadata""" + + +class InProgressRunAgentConfigJobAgentConfigAgentEnvironment(BaseModel): + """Environment configuration to use for this agent""" + + environment_variables: Optional[Dict[str, str]] = None + """Environment variables to set when launching the agent.""" + + secrets: Optional[Dict[str, str]] = None + """Secrets to inject as environment variables when launching the agent. + + Map of environment variable names to secret IDs. + """ + + +class InProgressRunAgentConfigJobAgentConfig(BaseModel): + """Configuration for an agent in a benchmark job""" + + name: str + """Name of the agent""" + + type: Literal["job_agent"] + + agent_environment: Optional[InProgressRunAgentConfigJobAgentConfigAgentEnvironment] = None + """Environment configuration to use for this agent""" + + agent_id: Optional[str] = None + """ID of the agent to use (optional if agent exists by name)""" + + kwargs: Optional[Dict[str, str]] = None + """Additional kwargs for agent configuration""" + + api_model_name: Optional[str] = FieldInfo(alias="model_name", default=None) + """Model name override for this agent""" + + timeout_seconds: Optional[float] = None + """Timeout in seconds for this agent""" + + +InProgressRunAgentConfig: TypeAlias = Annotated[ + Union[InProgressRunAgentConfigExternalAPIAgentConfig, InProgressRunAgentConfigJobAgentConfig, None], + PropertyInfo(discriminator="type"), +] + + +class InProgressRun(BaseModel): + """ + A lightweight view of a benchmark run currently in progress, showing basic execution details without full outcome data. + """ + + benchmark_run_id: str + """The ID of the benchmark run.""" + + start_time_ms: int + """Start time (Unix milliseconds).""" + + state: Literal["running", "canceled", "completed"] + """The current state of the run.""" + + agent_config: Optional[InProgressRunAgentConfig] = None + """Agent configuration used for this run. + + Specifies whether the run was driven by an external API agent or a job-defined + agent. + """ + + duration_ms: Optional[int] = None + """Duration so far in milliseconds.""" + + +class JobSourceHarborJobSource(BaseModel): + """Harbor job source with inline YAML configuration""" + + inline_yaml: str + """The Harbor job configuration as inline YAML content""" + + type: Literal["harbor"] + + +class JobSourceBenchmarkDefJobSource(BaseModel): + """Benchmark definition job source""" + + benchmark_id: str + """The ID of the benchmark definition""" + + type: Literal["benchmark"] + + benchmark_name: Optional[str] = None + """Optional user-provided name for the benchmark definition""" + + +class JobSourceScenariosJobSource(BaseModel): + """Scenarios job source with a list of scenario definition IDs""" + + scenario_ids: List[str] + """List of scenario definition IDs to execute""" + + type: Literal["scenarios"] + + +JobSource: TypeAlias = Annotated[ + Union[JobSourceHarborJobSource, JobSourceBenchmarkDefJobSource, JobSourceScenariosJobSource, None], + PropertyInfo(discriminator="type"), +] + + +class JobSpecAgentConfigAgentEnvironment(BaseModel): + """Environment configuration to use for this agent""" + + environment_variables: Optional[Dict[str, str]] = None + """Environment variables to set when launching the agent.""" + + secrets: Optional[Dict[str, str]] = None + """Secrets to inject as environment variables when launching the agent. + + Map of environment variable names to secret IDs. + """ + + +class JobSpecAgentConfig(BaseModel): + """Configuration for an agent in a benchmark job""" + + name: str + """Name of the agent""" + + type: Literal["job_agent"] + + agent_environment: Optional[JobSpecAgentConfigAgentEnvironment] = None + """Environment configuration to use for this agent""" + + agent_id: Optional[str] = None + """ID of the agent to use (optional if agent exists by name)""" + + kwargs: Optional[Dict[str, str]] = None + """Additional kwargs for agent configuration""" + + api_model_name: Optional[str] = FieldInfo(alias="model_name", default=None) + """Model name override for this agent""" + + timeout_seconds: Optional[float] = None + """Timeout in seconds for this agent""" + + +class JobSpecOrchestratorConfig(BaseModel): + """Orchestrator configuration""" + + n_attempts: Optional[int] = None + """Number of retry attempts on failure (default: 0). + + This is the retry policy for failed scenarios. Default is 0. + """ + + n_concurrent_trials: Optional[int] = None + """Number of concurrent trials to run (default: 1). + + Controls parallelism for scenario execution. Default is 1. + """ + + quiet: Optional[bool] = None + """Suppress verbose output (default: false)""" + + timeout_multiplier: Optional[float] = None + """Timeout multiplier for retries (default: 1.0). + + Each retry will multiply the timeout by this factor. + """ + + +class JobSpec(BaseModel): + """The resolved job specification. + + Contains scenarios, agents, and orchestrator config. + """ + + agent_configs: List[JobSpecAgentConfig] + """Agent configurations for this job""" + + scenario_ids: List[str] + """List of scenario IDs to execute""" + + orchestrator_config: Optional[JobSpecOrchestratorConfig] = None + """Orchestrator configuration""" + + +class BenchmarkJobView(BaseModel): + """ + A BenchmarkJobView represents a benchmark job that runs a set of scenarios entirely on runloop. + """ + + id: str + """The ID of the BenchmarkJob.""" + + create_time_ms: int + """Timestamp when job was created (Unix milliseconds).""" + + name: str + """The unique name of the BenchmarkJob.""" + + state: Literal["initializing", "queued", "running", "completed", "failed", "cancelled", "timeout"] + """The current state of the benchmark job.""" + + benchmark_outcomes: Optional[List[BenchmarkOutcome]] = None + """Detailed outcome data for each benchmark run created by this job. + + Includes per-agent results and scenario-level details. + """ + + failure_reason: Optional[str] = None + """Failure reason if job failed.""" + + in_progress_runs: Optional[List[InProgressRun]] = None + """Benchmark runs currently in progress for this job. + + Shows runs that have not yet completed. + """ + + job_source: Optional[JobSource] = None + """The source configuration that was used to create this job. + + Either Harbor YAML or benchmark definition reference. + """ + + job_spec: Optional[JobSpec] = None + """The resolved job specification. + + Contains scenarios, agents, and orchestrator config. + """ diff --git a/src/runloop_api_client/types/benchmark_list_params.py b/src/runloop_api_client/types/benchmark_list_params.py new file mode 100644 index 000000000..4e8b0c78b --- /dev/null +++ b/src/runloop_api_client/types/benchmark_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__ = ["BenchmarkListParams"] + + +class BenchmarkListParams(TypedDict, total=False): + limit: int + """The limit of items to return. Default is 20. Max is 5000.""" + + name: str + """Filter by name""" + + starting_after: str + """Load the next page of data starting after the item with the given ID.""" diff --git a/src/runloop_api_client/types/benchmark_list_public_params.py b/src/runloop_api_client/types/benchmark_list_public_params.py new file mode 100644 index 000000000..6dec4283b --- /dev/null +++ b/src/runloop_api_client/types/benchmark_list_public_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__ = ["BenchmarkListPublicParams"] + + +class BenchmarkListPublicParams(TypedDict, total=False): + limit: int + """The limit of items to return. Default is 20. Max is 5000.""" + + starting_after: str + """Load the next page of data starting after the item with the given ID.""" diff --git a/src/runloop_api_client/types/benchmark_run_list_params.py b/src/runloop_api_client/types/benchmark_run_list_params.py new file mode 100644 index 000000000..28d4e2e87 --- /dev/null +++ b/src/runloop_api_client/types/benchmark_run_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__ = ["BenchmarkRunListParams"] + + +class BenchmarkRunListParams(TypedDict, total=False): + benchmark_id: str + """The Benchmark ID to filter by.""" + + limit: int + """The limit of items to return. Default is 20. Max is 5000.""" + + name: str + """Filter by name""" + + starting_after: str + """Load the next page of data starting after the item with the given ID.""" diff --git a/src/runloop_api_client/types/benchmark_run_list_scenario_runs_params.py b/src/runloop_api_client/types/benchmark_run_list_scenario_runs_params.py new file mode 100644 index 000000000..c88c09167 --- /dev/null +++ b/src/runloop_api_client/types/benchmark_run_list_scenario_runs_params.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, TypedDict + +__all__ = ["BenchmarkRunListScenarioRunsParams"] + + +class BenchmarkRunListScenarioRunsParams(TypedDict, total=False): + limit: int + """The limit of items to return. Default is 20. Max is 5000.""" + + starting_after: str + """Load the next page of data starting after the item with the given ID.""" + + state: Literal["running", "scoring", "scored", "completed", "canceled", "timeout", "failed"] + """Filter by Scenario Run state""" diff --git a/src/runloop_api_client/types/benchmark_run_list_view.py b/src/runloop_api_client/types/benchmark_run_list_view.py new file mode 100644 index 000000000..e85506bab --- /dev/null +++ b/src/runloop_api_client/types/benchmark_run_list_view.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel +from .benchmark_run_view import BenchmarkRunView + +__all__ = ["BenchmarkRunListView"] + + +class BenchmarkRunListView(BaseModel): + has_more: bool + + runs: List[BenchmarkRunView] + """List of BenchmarkRuns matching filter.""" + + remaining_count: Optional[int] = None + + total_count: Optional[int] = None diff --git a/src/runloop_api_client/types/benchmark_run_view.py b/src/runloop_api_client/types/benchmark_run_view.py new file mode 100644 index 000000000..8eaaf4ad5 --- /dev/null +++ b/src/runloop_api_client/types/benchmark_run_view.py @@ -0,0 +1,58 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, Optional +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["BenchmarkRunView"] + + +class BenchmarkRunView(BaseModel): + """ + A BenchmarkRunView represents a run of a complete set of Scenarios, organized under a Benchmark or created by a BenchmarkJob. + """ + + id: str + """The ID of the BenchmarkRun.""" + + metadata: Dict[str, str] + """User defined metadata to attach to the benchmark run for organization.""" + + start_time_ms: int + """The time the benchmark run execution started (Unix timestamp milliseconds).""" + + state: Literal["running", "canceled", "completed"] + """The state of the BenchmarkRun.""" + + benchmark_id: Optional[str] = None + """The ID of the Benchmark definition. + + Present if run was created from a benchmark definition. + """ + + duration_ms: Optional[int] = None + """The duration for the BenchmarkRun to complete.""" + + environment_variables: Optional[Dict[str, str]] = None + """Environment variables used to run the benchmark.""" + + name: Optional[str] = None + """The name of the BenchmarkRun.""" + + purpose: Optional[str] = None + """Purpose of the run.""" + + score: Optional[float] = None + """The final score across the BenchmarkRun, present once completed. + + Calculated as sum of scenario scores / number of scenario runs. + """ + + secrets_provided: Optional[Dict[str, str]] = None + """User secrets used to run the benchmark. + + Example: {"DB_PASS": "DATABASE_PASSWORD"} would set the environment variable + 'DB_PASS' on all scenario devboxes to the value of the secret + 'DATABASE_PASSWORD'. + """ diff --git a/src/runloop_api_client/types/benchmark_start_run_params.py b/src/runloop_api_client/types/benchmark_start_run_params.py new file mode 100644 index 000000000..7655ff5ad --- /dev/null +++ b/src/runloop_api_client/types/benchmark_start_run_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 import Dict, Optional +from typing_extensions import Required, Annotated, TypedDict + +from .._utils import PropertyInfo +from .shared_params.run_profile import RunProfile + +__all__ = ["BenchmarkStartRunParams"] + + +class BenchmarkStartRunParams(TypedDict, total=False): + benchmark_id: Required[str] + """ID of the Benchmark to run.""" + + metadata: Optional[Dict[str, str]] + """User defined metadata to attach to the benchmark run for organization.""" + + run_name: Optional[str] + """Display name of the run.""" + + run_profile: Annotated[Optional[RunProfile], PropertyInfo(alias="runProfile")] + """Runtime configuration to use for this benchmark run""" diff --git a/src/runloop_api_client/types/benchmark_update_params.py b/src/runloop_api_client/types/benchmark_update_params.py new file mode 100644 index 000000000..ce9e8fb0c --- /dev/null +++ b/src/runloop_api_client/types/benchmark_update_params.py @@ -0,0 +1,42 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, Optional +from typing_extensions import TypedDict + +from .._types import SequenceNotStr + +__all__ = ["BenchmarkUpdateParams"] + + +class BenchmarkUpdateParams(TypedDict, total=False): + attribution: Optional[str] + """Attribution information for the benchmark. Pass in empty string to clear.""" + + description: Optional[str] + """Detailed description of the benchmark. Pass in empty string to clear.""" + + metadata: Optional[Dict[str, str]] + """User defined metadata to attach to the benchmark. Pass in empty map to clear.""" + + name: Optional[str] + """The unique name of the Benchmark. Cannot be blank.""" + + required_environment_variables: Optional[SequenceNotStr[str]] + """Environment variables required to run the benchmark. + + If any required variables are not supplied, the benchmark will fail to start. + Pass in empty list to clear. + """ + + required_secret_names: Optional[SequenceNotStr[str]] + """ + Secrets required to run the benchmark with (environment variable name will be + mapped to the your user secret by name). If any of these secrets are not + provided or the mapping is incorrect, the benchmark will fail to start. Pass in + empty list to clear. + """ + + scenario_ids: Optional[SequenceNotStr[str]] + """The Scenario IDs that make up the Benchmark. Pass in empty list to clear.""" diff --git a/src/runloop_api_client/types/benchmark_update_scenarios_params.py b/src/runloop_api_client/types/benchmark_update_scenarios_params.py new file mode 100644 index 000000000..2aca2b0d4 --- /dev/null +++ b/src/runloop_api_client/types/benchmark_update_scenarios_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 import Optional +from typing_extensions import TypedDict + +from .._types import SequenceNotStr + +__all__ = ["BenchmarkUpdateScenariosParams"] + + +class BenchmarkUpdateScenariosParams(TypedDict, total=False): + scenarios_to_add: Optional[SequenceNotStr[str]] + """Scenario IDs to add to the Benchmark.""" + + scenarios_to_remove: Optional[SequenceNotStr[str]] + """Scenario IDs to remove from the Benchmark.""" diff --git a/src/runloop_api_client/types/benchmark_view.py b/src/runloop_api_client/types/benchmark_view.py new file mode 100644 index 000000000..4150847ac --- /dev/null +++ b/src/runloop_api_client/types/benchmark_view.py @@ -0,0 +1,49 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, List, Optional + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["BenchmarkView"] + + +class BenchmarkView(BaseModel): + """ + A BenchmarkDefinitionView represents a grouped set of Scenarios that together form a Benchmark. + """ + + id: str + """The ID of the Benchmark.""" + + metadata: Dict[str, str] + """User defined metadata to attach to the benchmark for organization.""" + + name: str + """The name of the Benchmark.""" + + scenario_ids: List[str] = FieldInfo(alias="scenarioIds") + """List of Scenario IDs that make up the benchmark.""" + + attribution: Optional[str] = None + """Attribution information for the benchmark.""" + + description: Optional[str] = None + """Detailed description of the benchmark.""" + + is_public: Optional[bool] = None + """Whether this benchmark is public.""" + + required_environment_variables: Optional[List[str]] = None + """Required environment variables used to run the benchmark. + + If any required environment variables are missing, the benchmark will fail to + start. + """ + + required_secret_names: Optional[List[str]] = None + """Required secrets used to run the benchmark. + + If any required secrets are missing, the benchmark will fail to start. + """ diff --git a/src/runloop_api_client/types/blueprint_build_log.py b/src/runloop_api_client/types/blueprint_build_log.py new file mode 100644 index 000000000..7666ac65c --- /dev/null +++ b/src/runloop_api_client/types/blueprint_build_log.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["BlueprintBuildLog"] + + +class BlueprintBuildLog(BaseModel): + level: str + """Log line severity level.""" + + message: str + """Log line message.""" + + timestamp_ms: int + """Time of log (Unix timestamp milliseconds).""" diff --git a/src/runloop_api_client/types/blueprint_build_logs_list_view.py b/src/runloop_api_client/types/blueprint_build_logs_list_view.py new file mode 100644 index 000000000..5a227ef6a --- /dev/null +++ b/src/runloop_api_client/types/blueprint_build_logs_list_view.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List + +from .._models import BaseModel +from .blueprint_build_log import BlueprintBuildLog + +__all__ = ["BlueprintBuildLogsListView"] + + +class BlueprintBuildLogsListView(BaseModel): + blueprint_id: str + """ID of the Blueprint.""" + + logs: List[BlueprintBuildLog] + """List of logs generated during Blueprint build.""" diff --git a/src/runloop_api_client/types/blueprint_build_parameters.py b/src/runloop_api_client/types/blueprint_build_parameters.py new file mode 100644 index 000000000..b8fafb82f --- /dev/null +++ b/src/runloop_api_client/types/blueprint_build_parameters.py @@ -0,0 +1,119 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, List, Optional +from typing_extensions import Literal + +from .._models import BaseModel +from .shared.launch_parameters import LaunchParameters +from .shared.code_mount_parameters import CodeMountParameters + +__all__ = ["BlueprintBuildParameters", "BuildContext", "Service", "ServiceCredentials"] + + +class BuildContext(BaseModel): + """A build context backed by an Object.""" + + object_id: str + """The ID of an object, whose contents are to be used as a build context.""" + + type: Literal["object"] + + +class ServiceCredentials(BaseModel): + """The credentials of the container service.""" + + password: str + """The password of the container service.""" + + username: str + """The username of the container service.""" + + +class Service(BaseModel): + image: str + """The image of the container service.""" + + name: str + """The name of the container service.""" + + credentials: Optional[ServiceCredentials] = None + """The credentials of the container service.""" + + env: Optional[Dict[str, str]] = None + """The environment variables of the container service.""" + + options: Optional[str] = None + """Additional Docker container create options.""" + + port_mappings: Optional[List[str]] = None + """The port mappings of the container service. + + Port mappings are in the format of :. + """ + + +class BlueprintBuildParameters(BaseModel): + name: str + """Name of the Blueprint.""" + + base_blueprint_id: Optional[str] = None + """ + (Optional) ID of previously built blueprint to use as a base blueprint for this + build. + """ + + base_blueprint_name: Optional[str] = None + """ + (Optional) Name of previously built blueprint to use as a base blueprint for + this build. When set, this will load the latest successfully built Blueprint + with the given name. Only one of (base_blueprint_id, base_blueprint_name) should + be specified. + """ + + build_args: Optional[Dict[str, str]] = None + """(Optional) Arbitrary Docker build args to pass during build.""" + + build_context: Optional[BuildContext] = None + """A build context backed by an Object.""" + + code_mounts: Optional[List[CodeMountParameters]] = None + """A list of code mounts to be included in the Blueprint.""" + + dockerfile: Optional[str] = None + """Dockerfile contents to be used to build the Blueprint.""" + + file_mounts: Optional[Dict[str, str]] = None + """(Optional) Map of paths and file contents to write before setup.""" + + launch_parameters: Optional[LaunchParameters] = None + """Parameters to configure your Devbox at launch time.""" + + metadata: Optional[Dict[str, str]] = None + """(Optional) User defined metadata for the Blueprint.""" + + network_policy_id: Optional[str] = None + """(Optional) ID of the network policy to apply during blueprint build. + + This restricts network access during the build process. This does not affect + devboxes created from this blueprint; if you want devboxes created from this + blueprint to inherit the network policy, set the network_policy_id on the + blueprint launch parameters. + """ + + secrets: Optional[Dict[str, str]] = None + """(Optional) Map of mount IDs/environment variable names to secret names. + + Secrets will be available to commands during the build. Secrets are NOT stored + in the blueprint image. Example: {"DB_PASS": "DATABASE_PASSWORD"} makes the + secret 'DATABASE_PASSWORD' available as environment variable 'DB_PASS'. + """ + + services: Optional[List[Service]] = None + """(Optional) List of containerized services to include in the Blueprint. + + These services will be pre-pulled during the build phase for optimized startup + performance. + """ + + system_setup_commands: Optional[List[str]] = None + """A list of commands to run to set up your system.""" diff --git a/src/runloop_api_client/types/blueprint_create_from_inspection_params.py b/src/runloop_api_client/types/blueprint_create_from_inspection_params.py new file mode 100644 index 000000000..278fa676a --- /dev/null +++ b/src/runloop_api_client/types/blueprint_create_from_inspection_params.py @@ -0,0 +1,49 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, Optional +from typing_extensions import Required, TypedDict + +from .._types import SequenceNotStr +from .inspection_source_param import InspectionSourceParam +from .shared_params.launch_parameters import LaunchParameters + +__all__ = ["BlueprintCreateFromInspectionParams"] + + +class BlueprintCreateFromInspectionParams(TypedDict, total=False): + inspection_source: Required[InspectionSourceParam] + """(Optional) Use a RepositoryInspection a source of a Blueprint build. + + The Dockerfile will be automatically created based on the RepositoryInspection + contents. + """ + + name: Required[str] + """Name of the Blueprint.""" + + file_mounts: Optional[Dict[str, str]] + """(Optional) Map of paths and file contents to write before setup.""" + + launch_parameters: Optional[LaunchParameters] + """Parameters to configure your Devbox at launch time.""" + + metadata: Optional[Dict[str, str]] + """(Optional) User defined metadata for the Blueprint.""" + + network_policy_id: Optional[str] + """(Optional) ID of the network policy to apply during blueprint build. + + This restricts network access during the build process. + """ + + secrets: Optional[Dict[str, str]] + """(Optional) Map of mount IDs/environment variable names to secret names. + + Secrets can be used as environment variables in system_setup_commands. Example: + {"GITHUB_TOKEN": "gh_secret"} makes 'gh_secret' available as GITHUB_TOKEN. + """ + + system_setup_commands: Optional[SequenceNotStr[str]] + """A list of commands to run to set up your system.""" diff --git a/src/runloop_api_client/types/blueprint_create_params.py b/src/runloop_api_client/types/blueprint_create_params.py new file mode 100644 index 000000000..95c33e7dc --- /dev/null +++ b/src/runloop_api_client/types/blueprint_create_params.py @@ -0,0 +1,121 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, Iterable, Optional +from typing_extensions import Literal, Required, TypedDict + +from .._types import SequenceNotStr +from .shared_params.launch_parameters import LaunchParameters +from .shared_params.code_mount_parameters import CodeMountParameters + +__all__ = ["BlueprintCreateParams", "BuildContext", "Service", "ServiceCredentials"] + + +class BlueprintCreateParams(TypedDict, total=False): + name: Required[str] + """Name of the Blueprint.""" + + base_blueprint_id: Optional[str] + """ + (Optional) ID of previously built blueprint to use as a base blueprint for this + build. + """ + + base_blueprint_name: Optional[str] + """ + (Optional) Name of previously built blueprint to use as a base blueprint for + this build. When set, this will load the latest successfully built Blueprint + with the given name. Only one of (base_blueprint_id, base_blueprint_name) should + be specified. + """ + + build_args: Optional[Dict[str, str]] + """(Optional) Arbitrary Docker build args to pass during build.""" + + build_context: Optional[BuildContext] + """A build context backed by an Object.""" + + code_mounts: Optional[Iterable[CodeMountParameters]] + """A list of code mounts to be included in the Blueprint.""" + + dockerfile: Optional[str] + """Dockerfile contents to be used to build the Blueprint.""" + + file_mounts: Optional[Dict[str, str]] + """(Optional) Map of paths and file contents to write before setup.""" + + launch_parameters: Optional[LaunchParameters] + """Parameters to configure your Devbox at launch time.""" + + metadata: Optional[Dict[str, str]] + """(Optional) User defined metadata for the Blueprint.""" + + network_policy_id: Optional[str] + """(Optional) ID of the network policy to apply during blueprint build. + + This restricts network access during the build process. This does not affect + devboxes created from this blueprint; if you want devboxes created from this + blueprint to inherit the network policy, set the network_policy_id on the + blueprint launch parameters. + """ + + secrets: Optional[Dict[str, str]] + """(Optional) Map of mount IDs/environment variable names to secret names. + + Secrets will be available to commands during the build. Secrets are NOT stored + in the blueprint image. Example: {"DB_PASS": "DATABASE_PASSWORD"} makes the + secret 'DATABASE_PASSWORD' available as environment variable 'DB_PASS'. + """ + + services: Optional[Iterable[Service]] + """(Optional) List of containerized services to include in the Blueprint. + + These services will be pre-pulled during the build phase for optimized startup + performance. + """ + + system_setup_commands: Optional[SequenceNotStr[str]] + """A list of commands to run to set up your system.""" + + +class BuildContext(TypedDict, total=False): + """A build context backed by an Object.""" + + object_id: Required[str] + """The ID of an object, whose contents are to be used as a build context.""" + + type: Required[Literal["object"]] + + +class ServiceCredentials(TypedDict, total=False): + """The credentials of the container service.""" + + password: Required[str] + """The password of the container service.""" + + username: Required[str] + """The username of the container service.""" + + +class Service(TypedDict, total=False): + image: Required[str] + """The image of the container service.""" + + name: Required[str] + """The name of the container service.""" + + credentials: Optional[ServiceCredentials] + """The credentials of the container service.""" + + env: Optional[Dict[str, str]] + """The environment variables of the container service.""" + + options: Optional[str] + """Additional Docker container create options.""" + + port_mappings: Optional[SequenceNotStr[str]] + """The port mappings of the container service. + + Port mappings are in the format of :. + """ diff --git a/src/runloop_api_client/types/blueprint_list_params.py b/src/runloop_api_client/types/blueprint_list_params.py new file mode 100644 index 000000000..f72de7d2f --- /dev/null +++ b/src/runloop_api_client/types/blueprint_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__ = ["BlueprintListParams"] + + +class BlueprintListParams(TypedDict, total=False): + limit: int + """The limit of items to return. Default is 20. Max is 5000.""" + + name: str + """Filter by name""" + + starting_after: str + """Load the next page of data starting after the item with the given ID.""" + + status: str + """Filter by build status (queued, provisioning, building, failed, build_complete)""" diff --git a/src/runloop_api_client/types/blueprint_list_public_params.py b/src/runloop_api_client/types/blueprint_list_public_params.py new file mode 100644 index 000000000..e0f224f32 --- /dev/null +++ b/src/runloop_api_client/types/blueprint_list_public_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__ = ["BlueprintListPublicParams"] + + +class BlueprintListPublicParams(TypedDict, total=False): + limit: int + """The limit of items to return. Default is 20. Max is 5000.""" + + name: str + """Filter by name""" + + starting_after: str + """Load the next page of data starting after the item with the given ID.""" + + status: str + """Filter by build status (queued, provisioning, building, failed, build_complete)""" diff --git a/src/runloop_api_client/types/blueprint_list_view.py b/src/runloop_api_client/types/blueprint_list_view.py new file mode 100644 index 000000000..7d97314f5 --- /dev/null +++ b/src/runloop_api_client/types/blueprint_list_view.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel +from .blueprint_view import BlueprintView + +__all__ = ["BlueprintListView"] + + +class BlueprintListView(BaseModel): + blueprints: List[BlueprintView] + """List of blueprints matching filter.""" + + has_more: bool + + remaining_count: Optional[int] = None + + total_count: Optional[int] = None diff --git a/src/runloop_api_client/types/blueprint_preview_params.py b/src/runloop_api_client/types/blueprint_preview_params.py new file mode 100644 index 000000000..b2dc1713d --- /dev/null +++ b/src/runloop_api_client/types/blueprint_preview_params.py @@ -0,0 +1,121 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, Iterable, Optional +from typing_extensions import Literal, Required, TypedDict + +from .._types import SequenceNotStr +from .shared_params.launch_parameters import LaunchParameters +from .shared_params.code_mount_parameters import CodeMountParameters + +__all__ = ["BlueprintPreviewParams", "BuildContext", "Service", "ServiceCredentials"] + + +class BlueprintPreviewParams(TypedDict, total=False): + name: Required[str] + """Name of the Blueprint.""" + + base_blueprint_id: Optional[str] + """ + (Optional) ID of previously built blueprint to use as a base blueprint for this + build. + """ + + base_blueprint_name: Optional[str] + """ + (Optional) Name of previously built blueprint to use as a base blueprint for + this build. When set, this will load the latest successfully built Blueprint + with the given name. Only one of (base_blueprint_id, base_blueprint_name) should + be specified. + """ + + build_args: Optional[Dict[str, str]] + """(Optional) Arbitrary Docker build args to pass during build.""" + + build_context: Optional[BuildContext] + """A build context backed by an Object.""" + + code_mounts: Optional[Iterable[CodeMountParameters]] + """A list of code mounts to be included in the Blueprint.""" + + dockerfile: Optional[str] + """Dockerfile contents to be used to build the Blueprint.""" + + file_mounts: Optional[Dict[str, str]] + """(Optional) Map of paths and file contents to write before setup.""" + + launch_parameters: Optional[LaunchParameters] + """Parameters to configure your Devbox at launch time.""" + + metadata: Optional[Dict[str, str]] + """(Optional) User defined metadata for the Blueprint.""" + + network_policy_id: Optional[str] + """(Optional) ID of the network policy to apply during blueprint build. + + This restricts network access during the build process. This does not affect + devboxes created from this blueprint; if you want devboxes created from this + blueprint to inherit the network policy, set the network_policy_id on the + blueprint launch parameters. + """ + + secrets: Optional[Dict[str, str]] + """(Optional) Map of mount IDs/environment variable names to secret names. + + Secrets will be available to commands during the build. Secrets are NOT stored + in the blueprint image. Example: {"DB_PASS": "DATABASE_PASSWORD"} makes the + secret 'DATABASE_PASSWORD' available as environment variable 'DB_PASS'. + """ + + services: Optional[Iterable[Service]] + """(Optional) List of containerized services to include in the Blueprint. + + These services will be pre-pulled during the build phase for optimized startup + performance. + """ + + system_setup_commands: Optional[SequenceNotStr[str]] + """A list of commands to run to set up your system.""" + + +class BuildContext(TypedDict, total=False): + """A build context backed by an Object.""" + + object_id: Required[str] + """The ID of an object, whose contents are to be used as a build context.""" + + type: Required[Literal["object"]] + + +class ServiceCredentials(TypedDict, total=False): + """The credentials of the container service.""" + + password: Required[str] + """The password of the container service.""" + + username: Required[str] + """The username of the container service.""" + + +class Service(TypedDict, total=False): + image: Required[str] + """The image of the container service.""" + + name: Required[str] + """The name of the container service.""" + + credentials: Optional[ServiceCredentials] + """The credentials of the container service.""" + + env: Optional[Dict[str, str]] + """The environment variables of the container service.""" + + options: Optional[str] + """Additional Docker container create options.""" + + port_mappings: Optional[SequenceNotStr[str]] + """The port mappings of the container service. + + Port mappings are in the format of :. + """ diff --git a/src/runloop_api_client/types/blueprint_preview_view.py b/src/runloop_api_client/types/blueprint_preview_view.py new file mode 100644 index 000000000..7e4b0c53c --- /dev/null +++ b/src/runloop_api_client/types/blueprint_preview_view.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["BlueprintPreviewView"] + + +class BlueprintPreviewView(BaseModel): + dockerfile: str + """The Dockerfile contents that will built.""" diff --git a/src/runloop_api_client/types/blueprint_view.py b/src/runloop_api_client/types/blueprint_view.py new file mode 100644 index 000000000..851b09426 --- /dev/null +++ b/src/runloop_api_client/types/blueprint_view.py @@ -0,0 +1,93 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, List, Optional +from typing_extensions import Literal + +from .._models import BaseModel +from .blueprint_build_parameters import BlueprintBuildParameters + +__all__ = ["BlueprintView", "ContainerizedService", "ContainerizedServiceCredentials"] + + +class ContainerizedServiceCredentials(BaseModel): + """The credentials of the container service.""" + + password: str + """The password of the container service.""" + + username: str + """The username of the container service.""" + + +class ContainerizedService(BaseModel): + image: str + """The image of the container service.""" + + name: str + """The name of the container service.""" + + credentials: Optional[ContainerizedServiceCredentials] = None + """The credentials of the container service.""" + + env: Optional[Dict[str, str]] = None + """The environment variables of the container service.""" + + options: Optional[str] = None + """Additional Docker container create options.""" + + port_mappings: Optional[List[str]] = None + """The port mappings of the container service. + + Port mappings are in the format of :. + """ + + +class BlueprintView(BaseModel): + """Blueprints are ways to create customized starting points for Devboxes. + + They allow you to define custom starting points for Devboxes such that environment set up can be cached to improve Devbox boot times. + """ + + id: str + """The id of the Blueprint.""" + + create_time_ms: int + """Creation time of the Blueprint (Unix timestamp milliseconds).""" + + name: str + """The name of the Blueprint.""" + + parameters: BlueprintBuildParameters + """The parameters used to create Blueprint.""" + + state: Literal["created", "deleted"] + """The state of the Blueprint.""" + + status: Literal["queued", "provisioning", "building", "failed", "build_complete"] + """The status of the Blueprint build.""" + + base_blueprint_id: Optional[str] = None + """The ID of the base Blueprint.""" + + build_finish_time_ms: Optional[int] = None + """Build completion time of the Blueprint (Unix timestamp milliseconds).""" + + containerized_services: Optional[List[ContainerizedService]] = None + """List of ContainerizedServices available in the Blueprint. + + Services can be explicitly started when creating a Devbox. + """ + + devbox_capabilities: Optional[List[Literal["unknown", "computer_usage", "browser_usage", "docker_in_docker"]]] = ( + None + ) + """Capabilities that will be available on Devbox.""" + + failure_reason: Optional[Literal["out_of_memory", "out_of_disk", "build_failed"]] = None + """The failure reason if the Blueprint build failed, if any.""" + + is_public: Optional[bool] = None + """Whether this Blueprint is publicly accessible to all users.""" + + metadata: Optional[Dict[str, str]] = None + """User defined metadata associated with the blueprint.""" diff --git a/src/runloop_api_client/types/devbox_async_execution_detail_view.py b/src/runloop_api_client/types/devbox_async_execution_detail_view.py new file mode 100755 index 000000000..5239e4ed2 --- /dev/null +++ b/src/runloop_api_client/types/devbox_async_execution_detail_view.py @@ -0,0 +1,46 @@ +# 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__ = ["DevboxAsyncExecutionDetailView"] + + +class DevboxAsyncExecutionDetailView(BaseModel): + devbox_id: str + """Devbox id where command was executed.""" + + execution_id: str + """Ephemeral id of the execution in progress.""" + + status: Literal["queued", "running", "completed"] + """Current status of the execution.""" + + exit_status: Optional[int] = None + """Exit code of command execution. + + This field will remain unset until the execution has completed. + """ + + shell_name: Optional[str] = None + """Shell name.""" + + stderr: Optional[str] = None + """Standard error generated by command. + + This field will remain unset until the execution has completed. + """ + + stderr_truncated: Optional[bool] = None + """Indicates whether the stderr was truncated due to size limits.""" + + stdout: Optional[str] = None + """Standard out generated by command. + + This field will remain unset until the execution has completed. + """ + + stdout_truncated: Optional[bool] = None + """Indicates whether the stdout was truncated due to size limits.""" diff --git a/src/runloop_api_client/types/devbox_create_params.py b/src/runloop_api_client/types/devbox_create_params.py new file mode 100644 index 000000000..535ea3245 --- /dev/null +++ b/src/runloop_api_client/types/devbox_create_params.py @@ -0,0 +1,139 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, Iterable, Optional +from typing_extensions import Literal, Required, TypedDict + +from .shared_params.mount import Mount +from .shared_params.launch_parameters import LaunchParameters +from .shared_params.code_mount_parameters import CodeMountParameters + +__all__ = ["DevboxCreateParams", "Gateways", "Mcp", "Tunnel"] + + +class DevboxCreateParams(TypedDict, total=False): + blueprint_id: Optional[str] + """Blueprint ID to use for the Devbox. + + If none set, the Devbox will be created with the default Runloop Devbox image. + Only one of (Snapshot ID, Blueprint ID, Blueprint name) should be specified. + """ + + blueprint_name: Optional[str] + """Name of Blueprint to use for the Devbox. + + When set, this will load the latest successfully built Blueprint with the given + name. Only one of (Snapshot ID, Blueprint ID, Blueprint name) should be + specified. + """ + + code_mounts: Optional[Iterable[CodeMountParameters]] + """A list of code mounts to be included in the Devbox. Use mounts instead.""" + + entrypoint: Optional[str] + """ + (Optional) When specified, the Devbox will run this script as its main + executable. The devbox lifecycle will be bound to entrypoint, shutting down when + the process is complete. + """ + + environment_variables: Optional[Dict[str, str]] + """(Optional) Environment variables used to configure your Devbox.""" + + file_mounts: Optional[Dict[str, str]] + """Map of paths and file contents to write before setup. Use mounts instead.""" + + gateways: Optional[Dict[str, Gateways]] + """(Optional) Agent gateway specifications for credential proxying. + + Map key is the environment variable prefix (e.g., 'GWS_ANTHROPIC'). The agent + gateway will proxy requests to external APIs using the specified credential + without exposing the real API key. Example: {'GWS_ANTHROPIC': {'gateway': + 'anthropic', 'secret': 'my_claude_key'}} + """ + + launch_parameters: Optional[LaunchParameters] + """Parameters to configure the resources and launch time behavior of the Devbox.""" + + mcp: Optional[Dict[str, Mcp]] + """[Beta] (Optional) MCP specifications for MCP server access. + + Map key is the environment variable name for the MCP token envelope. Each spec + links an MCP config to a secret. The devbox will also receive RL_MCP_URL for the + MCP hub endpoint. Example: {'MCP_SECRET': {'mcp_config': 'github-readonly', + 'secret': 'MY_GITHUB_TOKEN'}} + """ + + metadata: Optional[Dict[str, str]] + """User defined metadata to attach to the devbox for organization.""" + + mounts: Optional[Iterable[Mount]] + """A list of mounts to be included in the Devbox.""" + + name: Optional[str] + """(Optional) A user specified name to give the Devbox.""" + + repo_connection_id: Optional[str] + """Repository connection id the devbox should source its base image from.""" + + secrets: Optional[Dict[str, str]] + """(Optional) Map of environment variable names to secret names. + + The secret values will be securely injected as environment variables in the + Devbox. Example: {"DB_PASS": "DATABASE_PASSWORD"} sets environment variable + 'DB_PASS' to the value of secret 'DATABASE_PASSWORD'. + """ + + snapshot_id: Optional[str] + """Snapshot ID to use for the Devbox. + + Only one of (Snapshot ID, Blueprint ID, Blueprint name) should be specified. + """ + + tunnel: Optional[Tunnel] + """(Optional) Configuration for creating a V2 tunnel at Devbox launch time. + + When specified, a tunnel will be automatically provisioned and the tunnel + details will be included in the Devbox response. + """ + + +class Gateways(TypedDict, total=False): + """ + GatewaySpec links an agent gateway configuration to a secret for credential proxying in a devbox. The agent gateway will proxy requests to external APIs using the specified credential without exposing the real API key. + """ + + gateway: Required[str] + """The gateway config to use. Can be a gateway config ID (gwc_xxx) or name.""" + + secret: Required[str] + """The secret containing the credential. Can be a secret ID or name.""" + + +class Mcp(TypedDict, total=False): + """ + [Beta] McpSpec links an MCP configuration to a secret for MCP server access in a devbox. The MCP hub will proxy requests to upstream MCP servers using the specified credential, with tool-level access control based on the MCP config's allowed_tools. + """ + + mcp_config: Required[str] + """The MCP config to use. Can be an MCP config ID (mcp_xxx) or name.""" + + secret: Required[str] + """The secret containing the MCP server credential. Can be a secret ID or name.""" + + +class Tunnel(TypedDict, total=False): + """(Optional) Configuration for creating a V2 tunnel at Devbox launch time. + + When specified, a tunnel will be automatically provisioned and the tunnel details will be included in the Devbox response. + """ + + auth_mode: Optional[Literal["open", "authenticated"]] + """Authentication mode for the tunnel. Defaults to 'public' if not specified.""" + + http_keep_alive: Optional[bool] + """ + When true, HTTP traffic through the tunnel counts as activity for idle lifecycle + policies, resetting the idle timer. Defaults to true if not specified. + """ diff --git a/src/runloop_api_client/types/devbox_create_ssh_key_response.py b/src/runloop_api_client/types/devbox_create_ssh_key_response.py new file mode 100755 index 000000000..90032165d --- /dev/null +++ b/src/runloop_api_client/types/devbox_create_ssh_key_response.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["DevboxCreateSSHKeyResponse"] + + +class DevboxCreateSSHKeyResponse(BaseModel): + id: str + """The ID of the Devbox.""" + + ssh_private_key: str + """The ssh private key, in PEM format.""" + + ssh_user: str + """The Linux user to use for SSH connections to this Devbox.""" + + url: str + """The host url of the Devbox that can be used for SSH.""" diff --git a/src/runloop_api_client/types/devbox_create_tunnel_params.py b/src/runloop_api_client/types/devbox_create_tunnel_params.py new file mode 100644 index 000000000..5700e37ac --- /dev/null +++ b/src/runloop_api_client/types/devbox_create_tunnel_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__ = ["DevboxCreateTunnelParams"] + + +class DevboxCreateTunnelParams(TypedDict, total=False): + port: Required[int] + """Devbox port that tunnel will expose.""" diff --git a/src/runloop_api_client/types/devbox_download_file_params.py b/src/runloop_api_client/types/devbox_download_file_params.py new file mode 100644 index 000000000..fb0566329 --- /dev/null +++ b/src/runloop_api_client/types/devbox_download_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__ = ["DevboxDownloadFileParams"] + + +class DevboxDownloadFileParams(TypedDict, total=False): + path: Required[str] + """The path on the Devbox filesystem to read the file from. + + Path is relative to user home directory. + """ diff --git a/src/runloop_api_client/types/devbox_enable_tunnel_params.py b/src/runloop_api_client/types/devbox_enable_tunnel_params.py new file mode 100644 index 000000000..4906c85ef --- /dev/null +++ b/src/runloop_api_client/types/devbox_enable_tunnel_params.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Literal, TypedDict + +__all__ = ["DevboxEnableTunnelParams"] + + +class DevboxEnableTunnelParams(TypedDict, total=False): + auth_mode: Optional[Literal["open", "authenticated"]] + """Authentication mode for the tunnel. Defaults to 'public' if not specified.""" + + http_keep_alive: Optional[bool] + """ + When true, HTTP traffic through the tunnel counts as activity for idle lifecycle + policies, resetting the idle timer. Defaults to true if not specified. + """ diff --git a/src/runloop_api_client/types/devbox_execute_async_params.py b/src/runloop_api_client/types/devbox_execute_async_params.py new file mode 100644 index 000000000..9de00ccf0 --- /dev/null +++ b/src/runloop_api_client/types/devbox_execute_async_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 Optional +from typing_extensions import Required, TypedDict + +__all__ = ["DevboxExecuteAsyncParams"] + + +class DevboxExecuteAsyncParams(TypedDict, total=False): + command: Required[str] + """The command to execute via the Devbox shell. + + By default, commands are run from the user home directory unless shell_name is + specified. If shell_name is specified the command is run from the directory + based on the recent state of the persistent shell. + """ + + attach_stdin: Optional[bool] + """Whether to attach stdin streaming for async commands. + + Not valid for execute_sync endpoint. Defaults to false if not specified. + """ + + shell_name: Optional[str] + """The name of the persistent shell to create or use if already created. + + When using a persistent shell, the command will run from the directory at the + end of the previous command and environment variables will be preserved. + """ diff --git a/src/runloop_api_client/types/devbox_execute_params.py b/src/runloop_api_client/types/devbox_execute_params.py new file mode 100644 index 000000000..69e373e5c --- /dev/null +++ b/src/runloop_api_client/types/devbox_execute_params.py @@ -0,0 +1,37 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Required, TypedDict + +__all__ = ["DevboxExecuteParams"] + + +class DevboxExecuteParams(TypedDict, total=False): + command: Required[str] + """The command to execute via the Devbox shell. + + By default, commands are run from the user home directory unless shell_name is + specified. If shell_name is specified the command is run from the directory + based on the recent state of the persistent shell. + """ + + command_id: Required[str] + """The command ID in UUIDv7 string format for idempotency and tracking""" + + last_n: str + """Last n lines of standard error / standard out to return (default: 100)""" + + optimistic_timeout: Optional[int] + """Timeout in seconds to wait for command completion, up to 25 seconds. + + Defaults to 25 seconds. Operation is not killed. + """ + + shell_name: Optional[str] + """The name of the persistent shell to create or use if already created. + + When using a persistent shell, the command will run from the directory at the + end of the previous command and environment variables will be preserved. + """ diff --git a/src/runloop_api_client/types/devbox_execute_sync_params.py b/src/runloop_api_client/types/devbox_execute_sync_params.py new file mode 100644 index 000000000..c911a151c --- /dev/null +++ b/src/runloop_api_client/types/devbox_execute_sync_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 Optional +from typing_extensions import Required, TypedDict + +__all__ = ["DevboxExecuteSyncParams"] + + +class DevboxExecuteSyncParams(TypedDict, total=False): + command: Required[str] + """The command to execute via the Devbox shell. + + By default, commands are run from the user home directory unless shell_name is + specified. If shell_name is specified the command is run from the directory + based on the recent state of the persistent shell. + """ + + attach_stdin: Optional[bool] + """Whether to attach stdin streaming for async commands. + + Not valid for execute_sync endpoint. Defaults to false if not specified. + """ + + shell_name: Optional[str] + """The name of the persistent shell to create or use if already created. + + When using a persistent shell, the command will run from the directory at the + end of the previous command and environment variables will be preserved. + """ diff --git a/src/runloop_api_client/types/devbox_execution_detail_view.py b/src/runloop_api_client/types/devbox_execution_detail_view.py new file mode 100644 index 000000000..8c56bf86b --- /dev/null +++ b/src/runloop_api_client/types/devbox_execution_detail_view.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__ = ["DevboxExecutionDetailView"] + + +class DevboxExecutionDetailView(BaseModel): + devbox_id: str + """Devbox id where command was executed.""" + + exit_status: int + """Exit status of command execution.""" + + stderr: str + """Standard error generated by command.""" + + stdout: str + """Standard out generated by command.""" + + shell_name: Optional[str] = None + """Shell name.""" diff --git a/src/runloop_api_client/types/devbox_list_disk_snapshots_params.py b/src/runloop_api_client/types/devbox_list_disk_snapshots_params.py new file mode 100644 index 000000000..d26c3fbd8 --- /dev/null +++ b/src/runloop_api_client/types/devbox_list_disk_snapshots_params.py @@ -0,0 +1,32 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Annotated, TypedDict + +from .._utils import PropertyInfo + +__all__ = ["DevboxListDiskSnapshotsParams"] + + +class DevboxListDiskSnapshotsParams(TypedDict, total=False): + devbox_id: str + """Devbox ID to filter by.""" + + limit: int + """The limit of items to return. Default is 20. Max is 5000.""" + + metadata_key: Annotated[str, PropertyInfo(alias="metadata[key]")] + """Filter snapshots by metadata key-value pair. + + Can be used multiple times for different keys. + """ + + metadata_key_in: Annotated[str, PropertyInfo(alias="metadata[key][in]")] + """Filter snapshots by metadata key with multiple possible values (OR condition).""" + + source_blueprint_id: str + """Source Blueprint ID to filter snapshots by.""" + + starting_after: str + """Load the next page of data starting after the item with the given ID.""" diff --git a/src/runloop_api_client/types/devbox_list_params.py b/src/runloop_api_client/types/devbox_list_params.py new file mode 100644 index 000000000..c508762da --- /dev/null +++ b/src/runloop_api_client/types/devbox_list_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 Literal, TypedDict + +__all__ = ["DevboxListParams"] + + +class DevboxListParams(TypedDict, total=False): + limit: int + """The limit of items to return. Default is 20. Max is 5000.""" + + starting_after: str + """Load the next page of data starting after the item with the given ID.""" + + status: Literal[ + "provisioning", "initializing", "running", "suspending", "suspended", "resuming", "failure", "shutdown" + ] + """Filter by status""" diff --git a/src/runloop_api_client/types/devbox_list_view.py b/src/runloop_api_client/types/devbox_list_view.py new file mode 100644 index 000000000..6bd522ee5 --- /dev/null +++ b/src/runloop_api_client/types/devbox_list_view.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel +from .devbox_view import DevboxView + +__all__ = ["DevboxListView"] + + +class DevboxListView(BaseModel): + devboxes: List[DevboxView] + """List of devboxes matching filter.""" + + has_more: bool + + remaining_count: Optional[int] = None + + total_count: Optional[int] = None diff --git a/src/runloop_api_client/types/devbox_read_file_contents_params.py b/src/runloop_api_client/types/devbox_read_file_contents_params.py new file mode 100644 index 000000000..04d364fa3 --- /dev/null +++ b/src/runloop_api_client/types/devbox_read_file_contents_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__ = ["DevboxReadFileContentsParams"] + + +class DevboxReadFileContentsParams(TypedDict, total=False): + file_path: Required[str] + """The path on the Devbox filesystem to read the file from. + + Path is relative to user home directory. + """ diff --git a/src/runloop_api_client/types/devbox_read_file_contents_response.py b/src/runloop_api_client/types/devbox_read_file_contents_response.py new file mode 100644 index 000000000..712d641c1 --- /dev/null +++ b/src/runloop_api_client/types/devbox_read_file_contents_response.py @@ -0,0 +1,7 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import TypeAlias + +__all__ = ["DevboxReadFileContentsResponse"] + +DevboxReadFileContentsResponse: TypeAlias = str diff --git a/src/runloop_api_client/types/devbox_remove_tunnel_params.py b/src/runloop_api_client/types/devbox_remove_tunnel_params.py new file mode 100644 index 000000000..eb58e2702 --- /dev/null +++ b/src/runloop_api_client/types/devbox_remove_tunnel_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__ = ["DevboxRemoveTunnelParams"] + + +class DevboxRemoveTunnelParams(TypedDict, total=False): + port: Required[int] + """Devbox port that tunnel will expose.""" diff --git a/src/runloop_api_client/types/devbox_resource_usage_view.py b/src/runloop_api_client/types/devbox_resource_usage_view.py new file mode 100644 index 000000000..fe5c83a57 --- /dev/null +++ b/src/runloop_api_client/types/devbox_resource_usage_view.py @@ -0,0 +1,48 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["DevboxResourceUsageView"] + + +class DevboxResourceUsageView(BaseModel): + id: str + """The devbox ID.""" + + disk_gb_seconds: int + """Disk usage in GB-seconds (total_elapsed_seconds multiplied by disk size in GB). + + Disk is billed for elapsed time since storage is consumed even when suspended. + """ + + memory_gb_seconds: int + """Memory usage in GB-seconds (total_active_seconds multiplied by memory in GB).""" + + start_time_ms: int + """The devbox creation time in milliseconds since epoch.""" + + status: str + """The current status of the devbox.""" + + total_active_seconds: int + """ + Total time in seconds the devbox was actively running (excludes time spent + suspended). + """ + + total_elapsed_seconds: int + """ + Total elapsed time in seconds from devbox creation to now (or end time if + terminated). Includes all time regardless of devbox state. + """ + + vcpu_seconds: int + """ + vCPU usage in vCPU-seconds (total_active_seconds multiplied by the number of + vCPUs). + """ + + end_time_ms: Optional[int] = None + """The devbox end time in milliseconds since epoch, or null if still running.""" diff --git a/src/runloop_api_client/types/devbox_send_std_in_result.py b/src/runloop_api_client/types/devbox_send_std_in_result.py new file mode 100644 index 000000000..2b675f9e9 --- /dev/null +++ b/src/runloop_api_client/types/devbox_send_std_in_result.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["DevboxSendStdInResult"] + + +class DevboxSendStdInResult(BaseModel): + devbox_id: str + """Devbox id where command is executing.""" + + execution_id: str + """Execution id that received the stdin.""" + + success: bool + """Whether the stdin was successfully sent.""" diff --git a/src/runloop_api_client/types/devbox_shutdown_params.py b/src/runloop_api_client/types/devbox_shutdown_params.py new file mode 100644 index 000000000..1a6af9336 --- /dev/null +++ b/src/runloop_api_client/types/devbox_shutdown_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__ = ["DevboxShutdownParams"] + + +class DevboxShutdownParams(TypedDict, total=False): + force: str + """If true, force shutdown even if snapshots are in progress. Defaults to false.""" diff --git a/src/runloop_api_client/types/devbox_snapshot_disk_async_params.py b/src/runloop_api_client/types/devbox_snapshot_disk_async_params.py new file mode 100644 index 000000000..7ad87ffb9 --- /dev/null +++ b/src/runloop_api_client/types/devbox_snapshot_disk_async_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, Optional +from typing_extensions import TypedDict + +__all__ = ["DevboxSnapshotDiskAsyncParams"] + + +class DevboxSnapshotDiskAsyncParams(TypedDict, total=False): + commit_message: Optional[str] + """(Optional) Commit message associated with the snapshot (max 1000 characters)""" + + metadata: Optional[Dict[str, str]] + """(Optional) Metadata used to describe the snapshot""" + + name: Optional[str] + """(Optional) A user specified name to give the snapshot""" diff --git a/src/runloop_api_client/types/devbox_snapshot_disk_params.py b/src/runloop_api_client/types/devbox_snapshot_disk_params.py new file mode 100644 index 000000000..6ce8804b4 --- /dev/null +++ b/src/runloop_api_client/types/devbox_snapshot_disk_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, Optional +from typing_extensions import TypedDict + +__all__ = ["DevboxSnapshotDiskParams"] + + +class DevboxSnapshotDiskParams(TypedDict, total=False): + commit_message: Optional[str] + """(Optional) Commit message associated with the snapshot (max 1000 characters)""" + + metadata: Optional[Dict[str, str]] + """(Optional) Metadata used to describe the snapshot""" + + name: Optional[str] + """(Optional) A user specified name to give the snapshot""" diff --git a/src/runloop_api_client/types/devbox_snapshot_list_view.py b/src/runloop_api_client/types/devbox_snapshot_list_view.py new file mode 100644 index 000000000..ca54f2eb1 --- /dev/null +++ b/src/runloop_api_client/types/devbox_snapshot_list_view.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel +from .devbox_snapshot_view import DevboxSnapshotView + +__all__ = ["DevboxSnapshotListView"] + + +class DevboxSnapshotListView(BaseModel): + has_more: bool + + snapshots: List[DevboxSnapshotView] + """List of snapshots matching filter.""" + + remaining_count: Optional[int] = None + + total_count: Optional[int] = None diff --git a/src/runloop_api_client/types/devbox_snapshot_view.py b/src/runloop_api_client/types/devbox_snapshot_view.py new file mode 100644 index 000000000..84cb3e8ee --- /dev/null +++ b/src/runloop_api_client/types/devbox_snapshot_view.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, Optional + +from .._models import BaseModel + +__all__ = ["DevboxSnapshotView"] + + +class DevboxSnapshotView(BaseModel): + id: str + """The unique identifier of the snapshot.""" + + create_time_ms: int + """Creation time of the Snapshot (Unix timestamp milliseconds).""" + + metadata: Dict[str, str] + """User defined metadata associated with the snapshot.""" + + source_devbox_id: str + """The source Devbox ID this snapshot was created from.""" + + commit_message: Optional[str] = None + """(Optional) The commit message of the snapshot (max 1000 characters).""" + + name: Optional[str] = None + """(Optional) The custom name of the snapshot.""" + + size_bytes: Optional[int] = None + """(Optional) The size of the snapshot in bytes, relative to the base blueprint.""" + + source_blueprint_id: Optional[str] = None + """(Optional) The source Blueprint ID this snapshot was created from.""" diff --git a/src/runloop_api_client/types/devbox_tunnel_view.py b/src/runloop_api_client/types/devbox_tunnel_view.py new file mode 100644 index 000000000..48e3b3ac8 --- /dev/null +++ b/src/runloop_api_client/types/devbox_tunnel_view.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["DevboxTunnelView"] + + +class DevboxTunnelView(BaseModel): + devbox_id: str + """ID of the Devbox the tunnel routes to.""" + + port: int + """Port of the Devbox the tunnel routes to.""" + + url: str + """Public url used to access Devbox.""" diff --git a/src/runloop_api_client/types/devbox_update_params.py b/src/runloop_api_client/types/devbox_update_params.py new file mode 100644 index 000000000..2fbdae310 --- /dev/null +++ b/src/runloop_api_client/types/devbox_update_params.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, Optional +from typing_extensions import TypedDict + +__all__ = ["DevboxUpdateParams"] + + +class DevboxUpdateParams(TypedDict, total=False): + metadata: Optional[Dict[str, str]] + """User defined metadata to attach to the devbox for organization.""" + + name: Optional[str] + """(Optional) A user specified name to give the Devbox.""" diff --git a/src/runloop_api_client/types/devbox_upload_file_params.py b/src/runloop_api_client/types/devbox_upload_file_params.py new file mode 100644 index 000000000..9e83fa318 --- /dev/null +++ b/src/runloop_api_client/types/devbox_upload_file_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 Required, TypedDict + +from .._types import FileTypes + +__all__ = ["DevboxUploadFileParams"] + + +class DevboxUploadFileParams(TypedDict, total=False): + path: Required[str] + """The path to write the file to on the Devbox. + + Path is relative to user home directory. + """ + + file: FileTypes diff --git a/src/runloop_api_client/types/devbox_view.py b/src/runloop_api_client/types/devbox_view.py new file mode 100644 index 000000000..cdd37cf1f --- /dev/null +++ b/src/runloop_api_client/types/devbox_view.py @@ -0,0 +1,136 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, List, Optional +from typing_extensions import Literal + +from .._models import BaseModel +from .tunnel_view import TunnelView +from .shared.launch_parameters import LaunchParameters + +__all__ = ["DevboxView", "StateTransition", "GatewaySpecs", "McpSpecs"] + + +class StateTransition(BaseModel): + status: Optional[ + Literal["provisioning", "initializing", "running", "suspending", "suspended", "resuming", "failure", "shutdown"] + ] = None + """The status of the Devbox. + + provisioning: Runloop is allocating and booting the necessary infrastructure + resources. initializing: Runloop defined boot scripts are running to enable the + environment for interaction. running: The Devbox is ready for interaction. + suspending: The Devbox disk is being snapshotted as part of suspension. + suspended: The Devbox disk is saved and no more active compute is being used for + the Devbox. resuming: The Devbox disk is being loaded as part of booting a + suspended Devbox. failure: The Devbox failed as part of booting or running user + requested actions. shutdown: The Devbox was successfully shutdown and no more + active compute is being used. + """ + + transition_time_ms: Optional[object] = None + """The time the status change occurred""" + + +class GatewaySpecs(BaseModel): + gateway_config_id: str + """The ID of the gateway config (e.g., gwc_123abc).""" + + secret_id: str + """The ID of the secret containing the credential.""" + + +class McpSpecs(BaseModel): + mcp_config_id: str + """The ID of the MCP config (e.g., mcp_123abc).""" + + secret_id: str + """The ID of the secret containing the credential.""" + + +class DevboxView(BaseModel): + """A Devbox represents a virtual development environment. + + It is an isolated sandbox that can be given to agents and used to run arbitrary code such as AI generated code. + """ + + id: str + """The ID of the Devbox.""" + + capabilities: List[Literal["unknown", "computer_usage", "browser_usage", "docker_in_docker"]] + """A list of capability groups this devbox has access to. + + This allows devboxes to be compatible with certain tools sets like computer + usage APIs. + """ + + create_time_ms: int + """Creation time of the Devbox (Unix timestamp milliseconds).""" + + end_time_ms: Optional[int] = None + """The time the Devbox finished execution (Unix timestamp milliseconds). + + Present if the Devbox is in a terminal state. + """ + + launch_parameters: LaunchParameters + """The launch parameters used to create the Devbox.""" + + metadata: Dict[str, str] + """The user defined Devbox metadata.""" + + state_transitions: List[StateTransition] + """A list of state transitions in order with durations""" + + status: Literal[ + "provisioning", "initializing", "running", "suspending", "suspended", "resuming", "failure", "shutdown" + ] + """The current status of the Devbox.""" + + blueprint_id: Optional[str] = None + """ + The Blueprint ID used in creation of the Devbox, if the devbox was created from + a Blueprint. + """ + + failure_reason: Optional[Literal["out_of_memory", "out_of_disk", "execution_failed"]] = None + """The failure reason if the Devbox failed, if the Devbox has a 'failure' status.""" + + gateway_specs: Optional[Dict[str, GatewaySpecs]] = None + """Gateway specifications configured for this devbox. + + Map key is the environment variable prefix (e.g., 'GWS_ANTHROPIC'). + """ + + initiator_id: Optional[str] = None + """The ID of the initiator that created the Devbox.""" + + initiator_type: Optional[Literal["unknown", "api", "scenario", "scoring_validation"]] = None + """The type of initiator that created the Devbox.""" + + mcp_specs: Optional[Dict[str, McpSpecs]] = None + """[Beta] MCP specifications configured for this devbox. + + Map key is the environment variable name for the MCP token envelope. Each spec + links an MCP config to a secret for MCP server access through the MCP hub. + """ + + name: Optional[str] = None + """The name of the Devbox.""" + + shutdown_reason: Optional[Literal["api_shutdown", "keep_alive_timeout", "entrypoint_exit", "idle"]] = None + """ + The shutdown reason if the Devbox shutdown, if the Devbox has a 'shutdown' + status. + """ + + snapshot_id: Optional[str] = None + """ + The Snapshot ID used in creation of the Devbox, if the devbox was created from a + Snapshot. + """ + + tunnel: Optional[TunnelView] = None + """ + V2 tunnel information if a tunnel was created at launch time or via the + createTunnel API. + """ diff --git a/src/runloop_api_client/types/devbox_wait_for_command_params.py b/src/runloop_api_client/types/devbox_wait_for_command_params.py new file mode 100644 index 000000000..2452de0c4 --- /dev/null +++ b/src/runloop_api_client/types/devbox_wait_for_command_params.py @@ -0,0 +1,28 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Optional +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["DevboxWaitForCommandParams"] + + +class DevboxWaitForCommandParams(TypedDict, total=False): + devbox_id: Required[str] + + statuses: Required[List[Literal["queued", "running", "completed"]]] + """The command execution statuses to wait for. + + At least one status must be provided. The command will be returned as soon as it + reaches any of the provided statuses. + """ + + last_n: str + """Last n lines of standard error / standard out to return (default: 100)""" + + timeout_seconds: Optional[int] + """(Optional) Timeout in seconds to wait for the status, up to 25 seconds. + + Defaults to 25 seconds. + """ diff --git a/src/runloop_api_client/types/devbox_write_file_contents_params.py b/src/runloop_api_client/types/devbox_write_file_contents_params.py new file mode 100644 index 000000000..f47a73839 --- /dev/null +++ b/src/runloop_api_client/types/devbox_write_file_contents_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__ = ["DevboxWriteFileContentsParams"] + + +class DevboxWriteFileContentsParams(TypedDict, total=False): + contents: Required[str] + """The UTF-8 string contents to write to the file.""" + + file_path: Required[str] + """The path to write the file to on the Devbox. + + Path is relative to user home directory. + """ diff --git a/src/runloop_api_client/types/devboxes/__init__.py b/src/runloop_api_client/types/devboxes/__init__.py new file mode 100644 index 000000000..0852b4c30 --- /dev/null +++ b/src/runloop_api_client/types/devboxes/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .browser_view import BrowserView as BrowserView +from .computer_view import ComputerView as ComputerView +from .log_list_params import LogListParams as LogListParams +from .browser_create_params import BrowserCreateParams as BrowserCreateParams +from .devbox_logs_list_view import DevboxLogsListView as DevboxLogsListView +from .execution_kill_params import ExecutionKillParams as ExecutionKillParams +from .computer_create_params import ComputerCreateParams as ComputerCreateParams +from .execution_update_chunk import ExecutionUpdateChunk as ExecutionUpdateChunk +from .disk_snapshot_list_params import DiskSnapshotListParams as DiskSnapshotListParams +from .execution_retrieve_params import ExecutionRetrieveParams as ExecutionRetrieveParams +from .disk_snapshot_update_params import DiskSnapshotUpdateParams as DiskSnapshotUpdateParams +from .execution_send_std_in_params import ExecutionSendStdInParams as ExecutionSendStdInParams +from .execution_execute_sync_params import ExecutionExecuteSyncParams as ExecutionExecuteSyncParams +from .execution_execute_async_params import ExecutionExecuteAsyncParams as ExecutionExecuteAsyncParams +from .computer_mouse_interaction_params import ComputerMouseInteractionParams as ComputerMouseInteractionParams +from .devbox_snapshot_async_status_view import DevboxSnapshotAsyncStatusView as DevboxSnapshotAsyncStatusView +from .computer_screen_interaction_params import ComputerScreenInteractionParams as ComputerScreenInteractionParams +from .computer_mouse_interaction_response import ComputerMouseInteractionResponse as ComputerMouseInteractionResponse +from .computer_keyboard_interaction_params import ComputerKeyboardInteractionParams as ComputerKeyboardInteractionParams +from .computer_screen_interaction_response import ComputerScreenInteractionResponse as ComputerScreenInteractionResponse +from .computer_keyboard_interaction_response import ( + ComputerKeyboardInteractionResponse as ComputerKeyboardInteractionResponse, +) +from .execution_stream_stderr_updates_params import ( + ExecutionStreamStderrUpdatesParams as ExecutionStreamStderrUpdatesParams, +) +from .execution_stream_stdout_updates_params import ( + ExecutionStreamStdoutUpdatesParams as ExecutionStreamStdoutUpdatesParams, +) diff --git a/src/runloop_api_client/types/devboxes/browser_create_params.py b/src/runloop_api_client/types/devboxes/browser_create_params.py new file mode 100644 index 000000000..523b03be0 --- /dev/null +++ b/src/runloop_api_client/types/devboxes/browser_create_params.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import TypedDict + +__all__ = ["BrowserCreateParams"] + + +class BrowserCreateParams(TypedDict, total=False): + name: Optional[str] + """The name to use for the created Devbox with a Browser.""" diff --git a/src/runloop_api_client/types/devboxes/browser_view.py b/src/runloop_api_client/types/devboxes/browser_view.py new file mode 100644 index 000000000..4486d76ec --- /dev/null +++ b/src/runloop_api_client/types/devboxes/browser_view.py @@ -0,0 +1,29 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from ..._models import BaseModel +from ..devbox_view import DevboxView + +__all__ = ["BrowserView"] + + +class BrowserView(BaseModel): + """ + A Browser represents a managed implementation of a browser like Chromiumon top of Devboxes. It includes the tunnel to the live screen and the underlying DevboxView. + """ + + connection_url: str + """ + The url to enable remote connection from browser automation tools like + playwright. + """ + + devbox: DevboxView + """The underlying devbox the browser setup is running on.""" + + live_view_url: str + """ + The url to view the browser window and enable user interactions via their own + browser. You can control the interactivity of the browser by adding or removing + 'view_only' query parameter. view_only=1 will allow interaction and view_only=0 + will disable interaction. + """ diff --git a/src/runloop_api_client/types/devboxes/computer_create_params.py b/src/runloop_api_client/types/devboxes/computer_create_params.py new file mode 100644 index 000000000..c2e32e035 --- /dev/null +++ b/src/runloop_api_client/types/devboxes/computer_create_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 Optional +from typing_extensions import Required, TypedDict + +__all__ = ["ComputerCreateParams", "DisplayDimensions"] + + +class ComputerCreateParams(TypedDict, total=False): + display_dimensions: Optional[DisplayDimensions] + """Customize the dimensions of the computer display.""" + + name: Optional[str] + """The name to use for the created computer.""" + + +class DisplayDimensions(TypedDict, total=False): + """Customize the dimensions of the computer display.""" + + display_height_px: Required[int] + """The height of the display being controlled by the model in pixels.""" + + display_width_px: Required[int] + """The width of the display being controlled by the model in pixels.""" diff --git a/src/runloop_api_client/types/devboxes/computer_keyboard_interaction_params.py b/src/runloop_api_client/types/devboxes/computer_keyboard_interaction_params.py new file mode 100644 index 000000000..c7c2a8f36 --- /dev/null +++ b/src/runloop_api_client/types/devboxes/computer_keyboard_interaction_params.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["ComputerKeyboardInteractionParams"] + + +class ComputerKeyboardInteractionParams(TypedDict, total=False): + action: Required[Literal["key", "type"]] + """The keyboard action to perform.""" + + text: Optional[str] + """The text to type or the key (with optional modifier) to press.""" diff --git a/src/runloop_api_client/types/devboxes/computer_keyboard_interaction_response.py b/src/runloop_api_client/types/devboxes/computer_keyboard_interaction_response.py new file mode 100644 index 000000000..6990bc74a --- /dev/null +++ b/src/runloop_api_client/types/devboxes/computer_keyboard_interaction_response.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__ = ["ComputerKeyboardInteractionResponse"] + + +class ComputerKeyboardInteractionResponse(BaseModel): + error: Optional[str] = None + + latest_screenshot_base64_img: Optional[str] = None + + output: Optional[str] = None diff --git a/src/runloop_api_client/types/devboxes/computer_mouse_interaction_params.py b/src/runloop_api_client/types/devboxes/computer_mouse_interaction_params.py new file mode 100644 index 000000000..a3a02279d --- /dev/null +++ b/src/runloop_api_client/types/devboxes/computer_mouse_interaction_params.py @@ -0,0 +1,35 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["ComputerMouseInteractionParams", "Coordinate"] + + +class ComputerMouseInteractionParams(TypedDict, total=False): + action: Required[ + Literal["mouse_move", "left_click", "left_click_drag", "right_click", "middle_click", "double_click"] + ] + """The mouse action to perform.""" + + coordinate: Optional[Coordinate] + """ + The x (pixels from the left) and y (pixels from the top) coordinates for the + mouse to move or click-drag. Required only by `action=mouse_move` or + `action=left_click_drag` + """ + + +class Coordinate(TypedDict, total=False): + """ + The x (pixels from the left) and y (pixels from the top) coordinates for the mouse to move or click-drag. Required only by + `action=mouse_move` or `action=left_click_drag` + """ + + x: Required[int] + """The x coordinate (pixels from the left) for the mouse to move or click-drag.""" + + y: Required[int] + """The y coordinate (pixels from the top) for the mouse to move or click-drag.""" diff --git a/src/runloop_api_client/types/devboxes/computer_mouse_interaction_response.py b/src/runloop_api_client/types/devboxes/computer_mouse_interaction_response.py new file mode 100644 index 000000000..cbaab9dca --- /dev/null +++ b/src/runloop_api_client/types/devboxes/computer_mouse_interaction_response.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__ = ["ComputerMouseInteractionResponse"] + + +class ComputerMouseInteractionResponse(BaseModel): + error: Optional[str] = None + + latest_screenshot_base64_img: Optional[str] = None + + output: Optional[str] = None diff --git a/src/runloop_api_client/types/devboxes/computer_screen_interaction_params.py b/src/runloop_api_client/types/devboxes/computer_screen_interaction_params.py new file mode 100644 index 000000000..8423249b9 --- /dev/null +++ b/src/runloop_api_client/types/devboxes/computer_screen_interaction_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 Literal, Required, TypedDict + +__all__ = ["ComputerScreenInteractionParams"] + + +class ComputerScreenInteractionParams(TypedDict, total=False): + action: Required[Literal["screenshot", "cursor_position"]] + """The screen action to perform.""" diff --git a/src/runloop_api_client/types/devboxes/computer_screen_interaction_response.py b/src/runloop_api_client/types/devboxes/computer_screen_interaction_response.py new file mode 100644 index 000000000..3ad4cb5cb --- /dev/null +++ b/src/runloop_api_client/types/devboxes/computer_screen_interaction_response.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__ = ["ComputerScreenInteractionResponse"] + + +class ComputerScreenInteractionResponse(BaseModel): + error: Optional[str] = None + + latest_screenshot_base64_img: Optional[str] = None + + output: Optional[str] = None diff --git a/src/runloop_api_client/types/devboxes/computer_view.py b/src/runloop_api_client/types/devboxes/computer_view.py new file mode 100644 index 000000000..4706d44a0 --- /dev/null +++ b/src/runloop_api_client/types/devboxes/computer_view.py @@ -0,0 +1,23 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from ..._models import BaseModel +from ..devbox_view import DevboxView + +__all__ = ["ComputerView"] + + +class ComputerView(BaseModel): + """ + A Computer represents an implementation of Anthropic Computer usage on top of Devboxes. It includes the tunnel to the live screen and the underlying DevboxView. + """ + + devbox: DevboxView + """The underlying devbox the computer setup is running on.""" + + live_screen_url: str + """The http tunnel to connect and view the live screen of the computer. + + You can control the interactivity of the browser by adding or removing + 'view_only' query parameter. view_only=1 will allow interaction and view_only=0 + will disable interaction. + """ diff --git a/src/runloop_api_client/types/devboxes/devbox_logs_list_view.py b/src/runloop_api_client/types/devboxes/devbox_logs_list_view.py new file mode 100644 index 000000000..fdcb72023 --- /dev/null +++ b/src/runloop_api_client/types/devboxes/devbox_logs_list_view.py @@ -0,0 +1,39 @@ +# 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__ = ["DevboxLogsListView", "Log"] + + +class Log(BaseModel): + level: str + """Log line severity level.""" + + source: Literal["setup_commands", "entrypoint", "exec", "files", "stats"] + """The source of the log.""" + + timestamp_ms: int + """Time of log (Unix timestamp milliseconds).""" + + cmd: Optional[str] = None + """The Command Executed""" + + cmd_id: Optional[str] = None + """Identifier of the associated command the log is sourced from.""" + + exit_code: Optional[int] = None + """The Exit Code of the command""" + + message: Optional[str] = None + """Log line message.""" + + shell_name: Optional[str] = None + """The Shell name the cmd executed in.""" + + +class DevboxLogsListView(BaseModel): + logs: List[Log] + """List of logs for the given devbox.""" diff --git a/src/runloop_api_client/types/devboxes/devbox_snapshot_async_status_view.py b/src/runloop_api_client/types/devboxes/devbox_snapshot_async_status_view.py new file mode 100644 index 000000000..11abaa62f --- /dev/null +++ b/src/runloop_api_client/types/devboxes/devbox_snapshot_async_status_view.py @@ -0,0 +1,20 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from ..devbox_snapshot_view import DevboxSnapshotView + +__all__ = ["DevboxSnapshotAsyncStatusView"] + + +class DevboxSnapshotAsyncStatusView(BaseModel): + status: Literal["in_progress", "error", "complete", "deleted"] + """The current status of the snapshot operation.""" + + error_message: Optional[str] = None + """Error message if the operation failed.""" + + snapshot: Optional[DevboxSnapshotView] = None + """The snapshot details if the operation completed successfully.""" diff --git a/src/runloop_api_client/types/devboxes/disk_snapshot_list_params.py b/src/runloop_api_client/types/devboxes/disk_snapshot_list_params.py new file mode 100644 index 000000000..73e60f457 --- /dev/null +++ b/src/runloop_api_client/types/devboxes/disk_snapshot_list_params.py @@ -0,0 +1,32 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Annotated, TypedDict + +from ..._utils import PropertyInfo + +__all__ = ["DiskSnapshotListParams"] + + +class DiskSnapshotListParams(TypedDict, total=False): + devbox_id: str + """Devbox ID to filter by.""" + + limit: int + """The limit of items to return. Default is 20. Max is 5000.""" + + metadata_key: Annotated[str, PropertyInfo(alias="metadata[key]")] + """Filter snapshots by metadata key-value pair. + + Can be used multiple times for different keys. + """ + + metadata_key_in: Annotated[str, PropertyInfo(alias="metadata[key][in]")] + """Filter snapshots by metadata key with multiple possible values (OR condition).""" + + source_blueprint_id: str + """Source Blueprint ID to filter snapshots by.""" + + starting_after: str + """Load the next page of data starting after the item with the given ID.""" diff --git a/src/runloop_api_client/types/devboxes/disk_snapshot_update_params.py b/src/runloop_api_client/types/devboxes/disk_snapshot_update_params.py new file mode 100644 index 000000000..548b6c08d --- /dev/null +++ b/src/runloop_api_client/types/devboxes/disk_snapshot_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, Optional +from typing_extensions import TypedDict + +__all__ = ["DiskSnapshotUpdateParams"] + + +class DiskSnapshotUpdateParams(TypedDict, total=False): + commit_message: Optional[str] + """(Optional) Commit message associated with the snapshot (max 1000 characters)""" + + metadata: Optional[Dict[str, str]] + """(Optional) Metadata used to describe the snapshot""" + + name: Optional[str] + """(Optional) A user specified name to give the snapshot""" diff --git a/src/runloop_api_client/types/devboxes/execution_execute_async_params.py b/src/runloop_api_client/types/devboxes/execution_execute_async_params.py new file mode 100644 index 000000000..e0ae30e53 --- /dev/null +++ b/src/runloop_api_client/types/devboxes/execution_execute_async_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 Optional +from typing_extensions import Required, TypedDict + +__all__ = ["ExecutionExecuteAsyncParams"] + + +class ExecutionExecuteAsyncParams(TypedDict, total=False): + command: Required[str] + """The command to execute via the Devbox shell. + + By default, commands are run from the user home directory unless shell_name is + specified. If shell_name is specified the command is run from the directory + based on the recent state of the persistent shell. + """ + + attach_stdin: Optional[bool] + """Whether to attach stdin streaming for async commands. + + Not valid for execute_sync endpoint. Defaults to false if not specified. + """ + + shell_name: Optional[str] + """The name of the persistent shell to create or use if already created. + + When using a persistent shell, the command will run from the directory at the + end of the previous command and environment variables will be preserved. + """ diff --git a/src/runloop_api_client/types/devboxes/execution_execute_sync_params.py b/src/runloop_api_client/types/devboxes/execution_execute_sync_params.py new file mode 100755 index 000000000..e0875a1c1 --- /dev/null +++ b/src/runloop_api_client/types/devboxes/execution_execute_sync_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 Optional +from typing_extensions import Required, TypedDict + +__all__ = ["ExecutionExecuteSyncParams"] + + +class ExecutionExecuteSyncParams(TypedDict, total=False): + command: Required[str] + """The command to execute via the Devbox shell. + + By default, commands are run from the user home directory unless shell_name is + specified. If shell_name is specified the command is run from the directory + based on the recent state of the persistent shell. + """ + + attach_stdin: Optional[bool] + """Whether to attach stdin streaming for async commands. + + Not valid for execute_sync endpoint. Defaults to false if not specified. + """ + + shell_name: Optional[str] + """The name of the persistent shell to create or use if already created. + + When using a persistent shell, the command will run from the directory at the + end of the previous command and environment variables will be preserved. + """ diff --git a/src/runloop_api_client/types/devboxes/execution_kill_params.py b/src/runloop_api_client/types/devboxes/execution_kill_params.py new file mode 100644 index 000000000..0df5c8615 --- /dev/null +++ b/src/runloop_api_client/types/devboxes/execution_kill_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 import Optional +from typing_extensions import Required, TypedDict + +__all__ = ["ExecutionKillParams"] + + +class ExecutionKillParams(TypedDict, total=False): + devbox_id: Required[str] + + kill_process_group: Optional[bool] + """Whether to kill the entire process group (default: false). + + If true, kills all processes in the same process group as the target process. + """ diff --git a/src/runloop_api_client/types/devboxes/execution_retrieve_params.py b/src/runloop_api_client/types/devboxes/execution_retrieve_params.py new file mode 100644 index 000000000..145d1a679 --- /dev/null +++ b/src/runloop_api_client/types/devboxes/execution_retrieve_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__ = ["ExecutionRetrieveParams"] + + +class ExecutionRetrieveParams(TypedDict, total=False): + devbox_id: Required[str] + + last_n: str + """Last n lines of standard error / standard out to return (default: 100)""" diff --git a/src/runloop_api_client/types/devboxes/execution_send_std_in_params.py b/src/runloop_api_client/types/devboxes/execution_send_std_in_params.py new file mode 100644 index 000000000..0d8358958 --- /dev/null +++ b/src/runloop_api_client/types/devboxes/execution_send_std_in_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 import Optional +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["ExecutionSendStdInParams"] + + +class ExecutionSendStdInParams(TypedDict, total=False): + devbox_id: Required[str] + + signal: Optional[Literal["EOF", "INTERRUPT"]] + """Signal to send to std in of the running execution.""" + + text: Optional[str] + """Text to send to std in of the running execution.""" diff --git a/src/runloop_api_client/types/devboxes/execution_stream_stderr_updates_params.py b/src/runloop_api_client/types/devboxes/execution_stream_stderr_updates_params.py new file mode 100644 index 000000000..ea879c046 --- /dev/null +++ b/src/runloop_api_client/types/devboxes/execution_stream_stderr_updates_params.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["ExecutionStreamStderrUpdatesParams"] + + +class ExecutionStreamStderrUpdatesParams(TypedDict, total=False): + devbox_id: Required[str] + + offset: str + """ + The byte offset to start the stream from (if unspecified, starts from the + beginning of the stream) + """ diff --git a/src/runloop_api_client/types/devboxes/execution_stream_stdout_updates_params.py b/src/runloop_api_client/types/devboxes/execution_stream_stdout_updates_params.py new file mode 100644 index 000000000..6d4d8111d --- /dev/null +++ b/src/runloop_api_client/types/devboxes/execution_stream_stdout_updates_params.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["ExecutionStreamStdoutUpdatesParams"] + + +class ExecutionStreamStdoutUpdatesParams(TypedDict, total=False): + devbox_id: Required[str] + + offset: str + """ + The byte offset to start the stream from (if unspecified, starts from the + beginning of the stream) + """ diff --git a/src/runloop_api_client/types/devboxes/execution_update_chunk.py b/src/runloop_api_client/types/devboxes/execution_update_chunk.py new file mode 100644 index 000000000..eda7add55 --- /dev/null +++ b/src/runloop_api_client/types/devboxes/execution_update_chunk.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__ = ["ExecutionUpdateChunk"] + + +class ExecutionUpdateChunk(BaseModel): + output: str + """The latest log stream chunk.""" + + offset: Optional[int] = None + """The byte offset of this chunk of log stream.""" diff --git a/src/runloop_api_client/types/devboxes/log_list_params.py b/src/runloop_api_client/types/devboxes/log_list_params.py new file mode 100644 index 000000000..be52499b2 --- /dev/null +++ b/src/runloop_api_client/types/devboxes/log_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__ = ["LogListParams"] + + +class LogListParams(TypedDict, total=False): + execution_id: str + """ID of execution to filter logs by.""" + + shell_name: str + """Shell Name to filter logs by.""" diff --git a/src/runloop_api_client/types/gateway_config_create_params.py b/src/runloop_api_client/types/gateway_config_create_params.py new file mode 100644 index 000000000..6a39055e4 --- /dev/null +++ b/src/runloop_api_client/types/gateway_config_create_params.py @@ -0,0 +1,41 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Required, TypedDict + +__all__ = ["GatewayConfigCreateParams", "AuthMechanism"] + + +class GatewayConfigCreateParams(TypedDict, total=False): + auth_mechanism: Required[AuthMechanism] + """How credentials should be applied to proxied requests. + + Specify the type ('header', 'bearer') and optional key field. + """ + + endpoint: Required[str] + """The target endpoint URL (e.g., 'https://api.anthropic.com').""" + + name: Required[str] + """The human-readable name for the GatewayConfig. + + Must be unique within your account. + """ + + description: Optional[str] + """Optional description for this gateway configuration.""" + + +class AuthMechanism(TypedDict, total=False): + """How credentials should be applied to proxied requests. + + Specify the type ('header', 'bearer') and optional key field. + """ + + type: Required[str] + """The type of authentication mechanism: 'header', 'bearer'.""" + + key: Optional[str] + """For 'header' type: the header name (e.g., 'x-api-key').""" diff --git a/src/runloop_api_client/types/gateway_config_list_params.py b/src/runloop_api_client/types/gateway_config_list_params.py new file mode 100644 index 000000000..cc8706b95 --- /dev/null +++ b/src/runloop_api_client/types/gateway_config_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__ = ["GatewayConfigListParams"] + + +class GatewayConfigListParams(TypedDict, total=False): + id: str + """Filter by ID.""" + + limit: int + """The limit of items to return. Default is 20. Max is 5000.""" + + name: str + """Filter by name (partial match supported).""" + + starting_after: str + """Load the next page of data starting after the item with the given ID.""" diff --git a/src/runloop_api_client/types/gateway_config_list_view.py b/src/runloop_api_client/types/gateway_config_list_view.py new file mode 100644 index 000000000..05511971f --- /dev/null +++ b/src/runloop_api_client/types/gateway_config_list_view.py @@ -0,0 +1,24 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel +from .gateway_config_view import GatewayConfigView + +__all__ = ["GatewayConfigListView"] + + +class GatewayConfigListView(BaseModel): + """A paginated list of GatewayConfigs.""" + + gateway_configs: List[GatewayConfigView] + """The list of GatewayConfigs.""" + + has_more: bool + """Whether there are more results available beyond this page.""" + + total_count: Optional[int] = None + """Total count of GatewayConfigs that match the query. + + Deprecated: will be removed in a future breaking change. + """ diff --git a/src/runloop_api_client/types/gateway_config_update_params.py b/src/runloop_api_client/types/gateway_config_update_params.py new file mode 100644 index 000000000..cdf385702 --- /dev/null +++ b/src/runloop_api_client/types/gateway_config_update_params.py @@ -0,0 +1,32 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Required, TypedDict + +__all__ = ["GatewayConfigUpdateParams", "AuthMechanism"] + + +class GatewayConfigUpdateParams(TypedDict, total=False): + auth_mechanism: Optional[AuthMechanism] + """New authentication mechanism for applying credentials to proxied requests.""" + + description: Optional[str] + """New description for this gateway configuration.""" + + endpoint: Optional[str] + """New target endpoint URL (e.g., 'https://api.anthropic.com').""" + + name: Optional[str] + """New name for the GatewayConfig. Must be unique within your account.""" + + +class AuthMechanism(TypedDict, total=False): + """New authentication mechanism for applying credentials to proxied requests.""" + + type: Required[str] + """The type of authentication mechanism: 'header', 'bearer'.""" + + key: Optional[str] + """For 'header' type: the header name (e.g., 'x-api-key').""" diff --git a/src/runloop_api_client/types/gateway_config_view.py b/src/runloop_api_client/types/gateway_config_view.py new file mode 100644 index 000000000..c8fcc6e3c --- /dev/null +++ b/src/runloop_api_client/types/gateway_config_view.py @@ -0,0 +1,47 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["GatewayConfigView", "AuthMechanism"] + + +class AuthMechanism(BaseModel): + """How credentials should be applied to proxied requests.""" + + type: str + """The type of authentication mechanism: 'header', 'bearer'.""" + + key: Optional[str] = None + """For 'header' type: the header name (e.g., 'x-api-key').""" + + +class GatewayConfigView(BaseModel): + """ + A GatewayConfig defines a configuration for proxying API requests through the agent gateway. It specifies the target endpoint and how credentials should be applied. + """ + + id: str + """The unique identifier of the GatewayConfig.""" + + auth_mechanism: AuthMechanism + """How credentials should be applied to proxied requests.""" + + create_time_ms: int + """Creation time of the GatewayConfig (Unix timestamp in milliseconds).""" + + endpoint: str + """The target endpoint URL (e.g., 'https://api.anthropic.com').""" + + name: str + """The human-readable name of the GatewayConfig. + + Unique per account (or globally for system configs). + """ + + account_id: Optional[str] = None + """The account ID that owns this config.""" + + description: Optional[str] = None + """Optional description for this gateway configuration.""" diff --git a/src/runloop_api_client/types/input_context.py b/src/runloop_api_client/types/input_context.py new file mode 100644 index 000000000..2daae5d45 --- /dev/null +++ b/src/runloop_api_client/types/input_context.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["InputContext"] + + +class InputContext(BaseModel): + """ + InputContextView specifies the problem statement along with all additional context for a Scenario. + """ + + problem_statement: str + """The problem statement for the Scenario.""" + + additional_context: Optional[object] = None + """Additional JSON structured input context.""" diff --git a/src/runloop_api_client/types/input_context_param.py b/src/runloop_api_client/types/input_context_param.py new file mode 100644 index 000000000..b0b495c4d --- /dev/null +++ b/src/runloop_api_client/types/input_context_param.py @@ -0,0 +1,20 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Required, TypedDict + +__all__ = ["InputContextParam"] + + +class InputContextParam(TypedDict, total=False): + """ + InputContextView specifies the problem statement along with all additional context for a Scenario. + """ + + problem_statement: Required[str] + """The problem statement for the Scenario.""" + + additional_context: Optional[object] + """Additional JSON structured input context.""" diff --git a/src/runloop_api_client/types/input_context_update_param.py b/src/runloop_api_client/types/input_context_update_param.py new file mode 100644 index 000000000..30580cf9a --- /dev/null +++ b/src/runloop_api_client/types/input_context_update_param.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import TypedDict + +__all__ = ["InputContextUpdateParam"] + + +class InputContextUpdateParam(TypedDict, total=False): + additional_context: Optional[object] + """Additional JSON structured input context.""" + + problem_statement: Optional[str] + """The problem statement for the Scenario.""" diff --git a/src/runloop_api_client/types/inspection_source_param.py b/src/runloop_api_client/types/inspection_source_param.py new file mode 100644 index 000000000..0d1308d8e --- /dev/null +++ b/src/runloop_api_client/types/inspection_source_param.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Required, TypedDict + +__all__ = ["InspectionSourceParam"] + + +class InspectionSourceParam(TypedDict, total=False): + """Use a RepositoryInspection a source of a Blueprint build.""" + + inspection_id: Required[str] + """The ID of a repository inspection.""" + + github_auth_token: Optional[str] + """GitHub authentication token for accessing private repositories.""" diff --git a/src/runloop_api_client/types/mcp_config_create_params.py b/src/runloop_api_client/types/mcp_config_create_params.py new file mode 100644 index 000000000..4a2dcd9e2 --- /dev/null +++ b/src/runloop_api_client/types/mcp_config_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 Optional +from typing_extensions import Required, TypedDict + +from .._types import SequenceNotStr + +__all__ = ["McpConfigCreateParams"] + + +class McpConfigCreateParams(TypedDict, total=False): + allowed_tools: Required[SequenceNotStr[str]] + """Glob patterns specifying which tools are allowed from this MCP server. + + Examples: ['*'] for all tools, ['github.search_*', 'github.get_*'] for specific + patterns. + """ + + endpoint: Required[str] + """The target MCP server endpoint URL (e.g., 'https://mcp.example.com').""" + + name: Required[str] + """The human-readable name for the McpConfig. + + Must be unique within your account. The first segment before '-' is used as the + service name for tool routing (e.g., 'github-readonly' uses 'github' as the + service name). + """ + + description: Optional[str] + """Optional description for this MCP configuration.""" diff --git a/src/runloop_api_client/types/mcp_config_list_params.py b/src/runloop_api_client/types/mcp_config_list_params.py new file mode 100644 index 000000000..8b786ba13 --- /dev/null +++ b/src/runloop_api_client/types/mcp_config_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__ = ["McpConfigListParams"] + + +class McpConfigListParams(TypedDict, total=False): + id: str + """Filter by ID.""" + + limit: int + """The limit of items to return. Default is 20. Max is 5000.""" + + name: str + """Filter by name (prefix match supported).""" + + starting_after: str + """Load the next page of data starting after the item with the given ID.""" diff --git a/src/runloop_api_client/types/mcp_config_list_view.py b/src/runloop_api_client/types/mcp_config_list_view.py new file mode 100644 index 000000000..4992698c5 --- /dev/null +++ b/src/runloop_api_client/types/mcp_config_list_view.py @@ -0,0 +1,24 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel +from .mcp_config_view import McpConfigView + +__all__ = ["McpConfigListView"] + + +class McpConfigListView(BaseModel): + """A paginated list of McpConfigs.""" + + has_more: bool + """Whether there are more results available beyond this page.""" + + mcp_configs: List[McpConfigView] + """The list of McpConfigs.""" + + total_count: Optional[int] = None + """Total count of McpConfigs that match the query. + + Deprecated: will be removed in a future breaking change. + """ diff --git a/src/runloop_api_client/types/mcp_config_update_params.py b/src/runloop_api_client/types/mcp_config_update_params.py new file mode 100644 index 000000000..234ca939f --- /dev/null +++ b/src/runloop_api_client/types/mcp_config_update_params.py @@ -0,0 +1,27 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import TypedDict + +from .._types import SequenceNotStr + +__all__ = ["McpConfigUpdateParams"] + + +class McpConfigUpdateParams(TypedDict, total=False): + allowed_tools: Optional[SequenceNotStr[str]] + """New glob patterns specifying which tools are allowed. + + Examples: ['*'] for all tools, ['github.search_*'] for specific patterns. + """ + + description: Optional[str] + """New description for this MCP configuration.""" + + endpoint: Optional[str] + """New target MCP server endpoint URL.""" + + name: Optional[str] + """New name for the McpConfig. Must be unique within your account.""" diff --git a/src/runloop_api_client/types/mcp_config_view.py b/src/runloop_api_client/types/mcp_config_view.py new file mode 100644 index 000000000..988537a59 --- /dev/null +++ b/src/runloop_api_client/types/mcp_config_view.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 + +__all__ = ["McpConfigView"] + + +class McpConfigView(BaseModel): + """ + An McpConfig defines a configuration for connecting to an upstream MCP (Model Context Protocol) server. It specifies the target endpoint and which tools are allowed. + """ + + id: str + """The unique identifier of the McpConfig.""" + + allowed_tools: List[str] + """ + Glob patterns specifying which tools are allowed from this MCP server (e.g., + ['github.search_*', 'github.get_*'] or ['*'] for all tools). + """ + + create_time_ms: int + """Creation time of the McpConfig (Unix timestamp in milliseconds).""" + + endpoint: str + """The target MCP server endpoint URL (e.g., 'https://mcp.example.com').""" + + name: str + """The human-readable name of the McpConfig. Unique per account.""" + + description: Optional[str] = None + """Optional description for this MCP configuration.""" diff --git a/src/runloop_api_client/types/network_policy_create_params.py b/src/runloop_api_client/types/network_policy_create_params.py new file mode 100644 index 000000000..0d8b1de2a --- /dev/null +++ b/src/runloop_api_client/types/network_policy_create_params.py @@ -0,0 +1,52 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Required, TypedDict + +from .._types import SequenceNotStr + +__all__ = ["NetworkPolicyCreateParams"] + + +class NetworkPolicyCreateParams(TypedDict, total=False): + name: Required[str] + """The human-readable name for the NetworkPolicy. + + Must be unique within the account. + """ + + allow_agent_gateway: Optional[bool] + """ + (Optional) If true, allows devbox egress to the agent gateway for credential + proxying. Defaults to false. + """ + + allow_all: Optional[bool] + """(Optional) If true, all egress traffic is allowed (ALLOW_ALL policy). + + Defaults to false. + """ + + allow_devbox_to_devbox: Optional[bool] + """ + (Optional) If true, allows traffic between the account's own devboxes via + tunnels. Defaults to false. If allow_all is true, this is automatically set to + true. + """ + + allow_mcp_gateway: Optional[bool] + """(Optional) If true, allows devbox egress to the MCP hub for MCP server access. + + Defaults to false. + """ + + allowed_hostnames: Optional[SequenceNotStr[str]] + """(Optional) DNS-based allow list with wildcard support. + + Examples: ['github.com', '*.npmjs.org']. + """ + + description: Optional[str] + """Optional description for the NetworkPolicy.""" diff --git a/src/runloop_api_client/types/network_policy_list_params.py b/src/runloop_api_client/types/network_policy_list_params.py new file mode 100644 index 000000000..160da795f --- /dev/null +++ b/src/runloop_api_client/types/network_policy_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__ = ["NetworkPolicyListParams"] + + +class NetworkPolicyListParams(TypedDict, total=False): + id: str + """Filter by ID.""" + + limit: int + """The limit of items to return. Default is 20. Max is 5000.""" + + name: str + """Filter by name (partial match supported).""" + + starting_after: str + """Load the next page of data starting after the item with the given ID.""" diff --git a/src/runloop_api_client/types/network_policy_list_view.py b/src/runloop_api_client/types/network_policy_list_view.py new file mode 100644 index 000000000..ba031d0a9 --- /dev/null +++ b/src/runloop_api_client/types/network_policy_list_view.py @@ -0,0 +1,24 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel +from .network_policy_view import NetworkPolicyView + +__all__ = ["NetworkPolicyListView"] + + +class NetworkPolicyListView(BaseModel): + """A list of NetworkPolicies with pagination information.""" + + has_more: bool + """Whether there are more results available.""" + + network_policies: List[NetworkPolicyView] + """The list of NetworkPolicies.""" + + total_count: Optional[int] = None + """Total count of items in this response. + + Deprecated: will be removed in a future breaking change. + """ diff --git a/src/runloop_api_client/types/network_policy_update_params.py b/src/runloop_api_client/types/network_policy_update_params.py new file mode 100644 index 000000000..0a622ca3e --- /dev/null +++ b/src/runloop_api_client/types/network_policy_update_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 Optional +from typing_extensions import TypedDict + +from .._types import SequenceNotStr + +__all__ = ["NetworkPolicyUpdateParams"] + + +class NetworkPolicyUpdateParams(TypedDict, total=False): + allow_agent_gateway: Optional[bool] + """If true, allows devbox egress to the agent gateway.""" + + allow_all: Optional[bool] + """If true, all egress traffic is allowed (ALLOW_ALL policy).""" + + allow_devbox_to_devbox: Optional[bool] + """If true, allows traffic between the account's own devboxes via tunnels.""" + + allow_mcp_gateway: Optional[bool] + """If true, allows devbox egress to the MCP hub.""" + + allowed_hostnames: Optional[SequenceNotStr[str]] + """Updated DNS-based allow list with wildcard support. + + Examples: ['github.com', '*.npmjs.org']. + """ + + description: Optional[str] + """Updated description for the NetworkPolicy.""" + + name: Optional[str] + """Updated human-readable name for the NetworkPolicy.""" diff --git a/src/runloop_api_client/types/network_policy_view.py b/src/runloop_api_client/types/network_policy_view.py new file mode 100644 index 000000000..0c3e25728 --- /dev/null +++ b/src/runloop_api_client/types/network_policy_view.py @@ -0,0 +1,58 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel + +__all__ = ["NetworkPolicyView", "Egress"] + + +class Egress(BaseModel): + """The egress rules for this policy.""" + + allow_agent_gateway: bool + """If true, allows devbox egress to the agent gateway for credential proxying.""" + + allow_all: bool + """If true, all egress traffic is allowed and other fields are ignored. + + Used for ALLOW_ALL policies. + """ + + allow_devbox_to_devbox: bool + """If true, allows traffic between the account's own devboxes via tunnels.""" + + allow_mcp_gateway: bool + """If true, allows devbox egress to the MCP hub for MCP server access.""" + + allowed_hostnames: List[str] + """DNS-based allow list with wildcard support. + + Examples: ['github.com', '*.npmjs.org', 'api.openai.com']. Empty list with + allow_all=false means no network access (DENY_ALL behavior). + """ + + +class NetworkPolicyView(BaseModel): + """A NetworkPolicy defines egress network access rules for devboxes. + + Policies can be applied to blueprints, devboxes, and snapshot resumes. + """ + + id: str + """The unique identifier of the NetworkPolicy.""" + + create_time_ms: int + """The creation time of the NetworkPolicy (Unix timestamp in milliseconds).""" + + egress: Egress + """The egress rules for this policy.""" + + name: str + """The human-readable name of the NetworkPolicy. Unique per account.""" + + update_time_ms: int + """Last update time of the NetworkPolicy (Unix timestamp in milliseconds).""" + + description: Optional[str] = None + """Optional description of the NetworkPolicy.""" diff --git a/src/runloop_api_client/types/object_create_params.py b/src/runloop_api_client/types/object_create_params.py new file mode 100644 index 000000000..ce154b4f3 --- /dev/null +++ b/src/runloop_api_client/types/object_create_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 import Dict, Optional +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["ObjectCreateParams"] + + +class ObjectCreateParams(TypedDict, total=False): + content_type: Required[Literal["unspecified", "text", "binary", "gzip", "tar", "tgz"]] + """The content type of the Object.""" + + name: Required[str] + """The name of the Object.""" + + metadata: Optional[Dict[str, str]] + """User defined metadata to attach to the object for organization.""" + + ttl_ms: Optional[int] + """ + Optional lifetime of the object in milliseconds, after which the object is + automatically deleted. Time starts ticking after the object is created. + """ diff --git a/src/runloop_api_client/types/object_download_params.py b/src/runloop_api_client/types/object_download_params.py new file mode 100644 index 000000000..3dd9e3472 --- /dev/null +++ b/src/runloop_api_client/types/object_download_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__ = ["ObjectDownloadParams"] + + +class ObjectDownloadParams(TypedDict, total=False): + duration_seconds: int + """Duration in seconds for the presigned URL validity (default: 3600).""" diff --git a/src/runloop_api_client/types/object_download_url_view.py b/src/runloop_api_client/types/object_download_url_view.py new file mode 100644 index 000000000..d1e726ca5 --- /dev/null +++ b/src/runloop_api_client/types/object_download_url_view.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["ObjectDownloadURLView"] + + +class ObjectDownloadURLView(BaseModel): + """A response containing a presigned download URL for an Object.""" + + download_url: str + """The presigned download URL for the Object.""" diff --git a/src/runloop_api_client/types/object_list_params.py b/src/runloop_api_client/types/object_list_params.py new file mode 100644 index 000000000..eca1c7cdd --- /dev/null +++ b/src/runloop_api_client/types/object_list_params.py @@ -0,0 +1,27 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, TypedDict + +__all__ = ["ObjectListParams"] + + +class ObjectListParams(TypedDict, total=False): + content_type: Literal["unspecified", "text", "binary", "gzip", "tar", "tgz"] + """Filter storage objects by content type.""" + + limit: int + """The limit of items to return. Default is 20. Max is 5000.""" + + name: str + """Filter storage objects by name (partial match supported).""" + + search: str + """Search by object ID or name.""" + + starting_after: str + """Load the next page of data starting after the item with the given ID.""" + + state: Literal["UPLOADING", "READ_ONLY", "DELETED", "ERROR"] + """Filter storage objects by state.""" diff --git a/src/runloop_api_client/types/object_list_public_params.py b/src/runloop_api_client/types/object_list_public_params.py new file mode 100644 index 000000000..67475b263 --- /dev/null +++ b/src/runloop_api_client/types/object_list_public_params.py @@ -0,0 +1,27 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, TypedDict + +__all__ = ["ObjectListPublicParams"] + + +class ObjectListPublicParams(TypedDict, total=False): + content_type: Literal["unspecified", "text", "binary", "gzip", "tar", "tgz"] + """Filter storage objects by content type.""" + + limit: int + """The limit of items to return. Default is 20. Max is 5000.""" + + name: str + """Filter storage objects by name (partial match supported).""" + + search: str + """Search by object ID or name.""" + + starting_after: str + """Load the next page of data starting after the item with the given ID.""" + + state: Literal["UPLOADING", "READ_ONLY", "DELETED", "ERROR"] + """Filter storage objects by state.""" diff --git a/src/runloop_api_client/types/object_list_view.py b/src/runloop_api_client/types/object_list_view.py new file mode 100644 index 000000000..689c0899b --- /dev/null +++ b/src/runloop_api_client/types/object_list_view.py @@ -0,0 +1,30 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel +from .object_view import ObjectView + +__all__ = ["ObjectListView"] + + +class ObjectListView(BaseModel): + """A paginated list of Objects.""" + + has_more: bool + """True if there are more results available beyond this page.""" + + objects: List[ObjectView] + """List of Object entities.""" + + remaining_count: Optional[int] = None + """Number of Objects remaining after this page. + + Deprecated: will be removed in a future breaking change. + """ + + total_count: Optional[int] = None + """Total number of Objects across all pages. + + Deprecated: will be removed in a future breaking change. + """ diff --git a/src/runloop_api_client/types/object_view.py b/src/runloop_api_client/types/object_view.py new file mode 100644 index 000000000..d4ced655f --- /dev/null +++ b/src/runloop_api_client/types/object_view.py @@ -0,0 +1,36 @@ +# 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__ = ["ObjectView"] + + +class ObjectView(BaseModel): + """An Object represents a stored data entity with metadata.""" + + id: str + """The unique identifier of the Object.""" + + content_type: Literal["unspecified", "text", "binary", "gzip", "tar", "tgz"] + """The content type of the Object.""" + + create_time_ms: int + """The creation time of the Object in milliseconds since epoch.""" + + name: str + """The name of the Object.""" + + state: Literal["UPLOADING", "READ_ONLY", "DELETED", "ERROR"] + """The current state of the Object.""" + + delete_after_time_ms: Optional[int] = None + """The time after which the Object will be deleted in milliseconds since epoch.""" + + size_bytes: Optional[int] = None + """The size of the Object content in bytes (null until uploaded).""" + + upload_url: Optional[str] = None + """Presigned URL for uploading content to S3 (only present on create).""" diff --git a/src/runloop_api_client/types/repository_connection_list_view.py b/src/runloop_api_client/types/repository_connection_list_view.py new file mode 100644 index 000000000..eea040bc4 --- /dev/null +++ b/src/runloop_api_client/types/repository_connection_list_view.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel +from .repository_connection_view import RepositoryConnectionView + +__all__ = ["RepositoryConnectionListView"] + + +class RepositoryConnectionListView(BaseModel): + has_more: bool + + repositories: List[RepositoryConnectionView] + """List of repositories matching filter.""" + + remaining_count: Optional[int] = None + + total_count: Optional[int] = None diff --git a/src/runloop_api_client/types/repository_connection_view.py b/src/runloop_api_client/types/repository_connection_view.py new file mode 100644 index 000000000..e126071e8 --- /dev/null +++ b/src/runloop_api_client/types/repository_connection_view.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["RepositoryConnectionView"] + + +class RepositoryConnectionView(BaseModel): + """The ID of the Repository.""" + + id: str + """The ID of the Repository.""" + + name: str + """The name of the Repository.""" + + owner: str + """The account owner of the Repository.""" diff --git a/src/runloop_api_client/types/repository_create_params.py b/src/runloop_api_client/types/repository_create_params.py new file mode 100644 index 000000000..fc241739e --- /dev/null +++ b/src/runloop_api_client/types/repository_create_params.py @@ -0,0 +1,22 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Required, TypedDict + +__all__ = ["RepositoryCreateParams"] + + +class RepositoryCreateParams(TypedDict, total=False): + name: Required[str] + """Name of the repository.""" + + owner: Required[str] + """Account owner of the repository.""" + + blueprint_id: Optional[str] + """ID of blueprint to use as base for resulting RepositoryVersion blueprint.""" + + github_auth_token: Optional[str] + """GitHub authentication token for accessing private repositories.""" diff --git a/src/runloop_api_client/types/repository_inspect_params.py b/src/runloop_api_client/types/repository_inspect_params.py new file mode 100644 index 000000000..c42103dab --- /dev/null +++ b/src/runloop_api_client/types/repository_inspect_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 Optional +from typing_extensions import TypedDict + +__all__ = ["RepositoryInspectParams"] + + +class RepositoryInspectParams(TypedDict, total=False): + github_auth_token: Optional[str] + """GitHub authentication token for accessing private repositories.""" diff --git a/src/runloop_api_client/types/repository_inspection_details.py b/src/runloop_api_client/types/repository_inspection_details.py new file mode 100644 index 000000000..0870ce693 --- /dev/null +++ b/src/runloop_api_client/types/repository_inspection_details.py @@ -0,0 +1,83 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, List, Optional +from typing_extensions import Literal + +from .._models import BaseModel +from .repository_manifest_view import RepositoryManifestView + +__all__ = ["RepositoryInspectionDetails", "WorkflowContexts", "WorkflowContextsActionsContext"] + + +class WorkflowContextsActionsContext(BaseModel): + """Details about actions processing for this workflow.""" + + actions_skipped_unnecessary: List[str] + """ + Actions that were skipped because they were unnecessary (e.g., upload + artifacts). + """ + + actions_taken: List[str] + """Actions that were translated into commands and executed.""" + + actions_unknown: List[str] + """ + Actions that were not understood and skipped because we did not know what to do. + """ + + +class WorkflowContexts(BaseModel): + """ + Workflow context containing file name and details about actions processing during inspection. + """ + + actions_context: WorkflowContextsActionsContext + """Details about actions processing for this workflow.""" + + file_name: str + """The file name of the workflow.""" + + +class RepositoryInspectionDetails(BaseModel): + id: str + """The ID of the inspection.""" + + commit_sha: str + """The sha of the inspected version of the Repository.""" + + inspected_at: int + """Inspection time of the Repository Version (Unix timestamp milliseconds).""" + + repository_manifest: RepositoryManifestView + """Repository manifest containing container config and workspace details.""" + + status: Literal[ + "invalid", + "repo_auth_pending", + "repo_authentication_failure", + "repo_access_failure", + "inspection_pending", + "inspection_failed", + "inspection_success", + "inspection_user_manifest_added", + ] + """The status of the repository inspection.""" + + blueprint_id: Optional[str] = None + """The blueprint ID associated with this inspection if successful.""" + + blueprint_name: Optional[str] = None + """The blueprint name associated with this inspection if successful.""" + + build_status: Optional[Literal["image_building", "image_build_success", "image_build_failure"]] = None + """The status of the linked Blueprint build.""" + + user_manifest: Optional[RepositoryManifestView] = None + """ + User uploaded repository manifest containing container config and workspace + details. + """ + + workflow_contexts: Optional[Dict[str, WorkflowContexts]] = None + """Workflow contexts mapping workflow names to their processing details.""" diff --git a/src/runloop_api_client/types/repository_inspection_list_view.py b/src/runloop_api_client/types/repository_inspection_list_view.py new file mode 100644 index 000000000..e6a7108b7 --- /dev/null +++ b/src/runloop_api_client/types/repository_inspection_list_view.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List + +from .._models import BaseModel +from .repository_inspection_details import RepositoryInspectionDetails + +__all__ = ["RepositoryInspectionListView"] + + +class RepositoryInspectionListView(BaseModel): + inspections: List[RepositoryInspectionDetails] + """List of inspections for this repository.""" diff --git a/src/runloop_api_client/types/repository_list_params.py b/src/runloop_api_client/types/repository_list_params.py new file mode 100644 index 000000000..d5f7b248a --- /dev/null +++ b/src/runloop_api_client/types/repository_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__ = ["RepositoryListParams"] + + +class RepositoryListParams(TypedDict, total=False): + limit: int + """The limit of items to return. Default is 20. Max is 5000.""" + + name: str + """Filter by repository name""" + + owner: str + """Filter by repository owner""" + + starting_after: str + """Load the next page of data starting after the item with the given ID.""" diff --git a/src/runloop_api_client/types/repository_manifest_view.py b/src/runloop_api_client/types/repository_manifest_view.py new file mode 100644 index 000000000..acb862672 --- /dev/null +++ b/src/runloop_api_client/types/repository_manifest_view.py @@ -0,0 +1,174 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, List, Optional +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = [ + "RepositoryManifestView", + "ContainerConfig", + "Language", + "Workspace", + "WorkspaceDevCommands", + "ContainerizedService", + "ContainerizedServiceCredentials", +] + + +class ContainerConfig(BaseModel): + """Container configuration specifying the base image and setup commands.""" + + base_image_name: str + """The name of the base image. + + Should be one of the GitHub public images like ubuntu-latest, ubuntu-24.04, + ubuntu-22.04, windows-latest, windows-2022, macos-latest etc. + """ + + architecture: Optional[Literal["x86_64", "arm64"]] = None + """The target architecture for the Repository Container.""" + + setup_commands: Optional[List[str]] = None + """ + Commands to run to setup the base container such as installing necessary + toolchains (e.g. apt install). + """ + + +class Language(BaseModel): + language: Optional[str] = None + + version: Optional[str] = None + + +class WorkspaceDevCommands(BaseModel): + """ + Extracted common commands important to the developer life cycle like linting, testing, building, etc. + """ + + build: Optional[List[str]] = None + """Build command (e.g. npm run build).""" + + install: Optional[List[str]] = None + """Installation command (e.g. pip install -r requirements.txt).""" + + lint: Optional[List[str]] = None + """Lint command (e.g. flake8).""" + + scripts: Optional[List[str]] = None + """Script commands.""" + + test: Optional[List[str]] = None + """Test command (e.g. pytest).""" + + +class Workspace(BaseModel): + """ + A workspace is a buildable unit of code within a repository and often represents a deployable unit of code like a backend service or a frontend app. + """ + + build_tool: List[str] + """Name of the build tool used (e.g. pip, npm).""" + + dev_commands: Optional[WorkspaceDevCommands] = None + """ + Extracted common commands important to the developer life cycle like linting, + testing, building, etc. + """ + + name: Optional[str] = None + """Name of the workspace. + + Can be empty if the workspace is the root of the repository. Only necessary for + monorepo style repositories. + """ + + path: Optional[str] = None + """Path to the workspace from the root of the repository. + + Can be empty if the workspace is the root of the repository. Only necessary for + monorepo style repositories. + """ + + workspace_refresh_commands: Optional[List[str]] = None + """ + Commands to run to refresh this workspace after pulling the latest changes to + the repository via git (e.g. npm install). + """ + + workspace_setup_commands: Optional[List[str]] = None + """ + Commands to run to setup this workspace after a fresh clone of the repository on + a new container such as installing necessary toolchains and dependencies (e.g. + npm install). + """ + + +class ContainerizedServiceCredentials(BaseModel): + """The credentials of the container service.""" + + password: str + """The password of the container service.""" + + username: str + """The username of the container service.""" + + +class ContainerizedService(BaseModel): + image: str + """The image of the container service.""" + + name: str + """The name of the container service.""" + + credentials: Optional[ContainerizedServiceCredentials] = None + """The credentials of the container service.""" + + env: Optional[Dict[str, str]] = None + """The environment variables of the container service.""" + + options: Optional[str] = None + """Additional Docker container create options.""" + + port_mappings: Optional[List[str]] = None + """The port mappings of the container service. + + Port mappings are in the format of :. + """ + + +class RepositoryManifestView(BaseModel): + """ + The repository manifest contains container configuration and workspace definitions for a repository. + """ + + container_config: ContainerConfig + """Container configuration specifying the base image and setup commands.""" + + languages: List[Language] + """List of required languages found in Repository.""" + + workspaces: List[Workspace] + """List of workspaces within the repository. + + Each workspace represents a buildable unit of code. + """ + + containerized_services: Optional[List[ContainerizedService]] = None + """List of discovered ContainerizedServices. + + Services can be explicitly started when creating a Devbox. + """ + + env_vars: Optional[Dict[str, str]] = None + """ + Qualified environment variables and values that should be set for this + repository to run correctly. + """ + + required_env_vars: Optional[List[str]] = None + """ + Missing environment variables that (may) be required for this repository to run + correctly. + """ diff --git a/src/runloop_api_client/types/repository_refresh_params.py b/src/runloop_api_client/types/repository_refresh_params.py new file mode 100644 index 000000000..e0eeae3e1 --- /dev/null +++ b/src/runloop_api_client/types/repository_refresh_params.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import TypedDict + +__all__ = ["RepositoryRefreshParams"] + + +class RepositoryRefreshParams(TypedDict, total=False): + blueprint_id: Optional[str] + """ID of blueprint to use as base for resulting RepositoryVersion blueprint.""" + + github_auth_token: Optional[str] + """GitHub authentication token for accessing private repositories.""" diff --git a/src/runloop_api_client/types/scenario_create_params.py b/src/runloop_api_client/types/scenario_create_params.py new file mode 100644 index 000000000..5b503a247 --- /dev/null +++ b/src/runloop_api_client/types/scenario_create_params.py @@ -0,0 +1,56 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, Optional +from typing_extensions import Literal, Required, TypedDict + +from .._types import SequenceNotStr +from .input_context_param import InputContextParam +from .scoring_contract_param import ScoringContractParam +from .scenario_environment_param import ScenarioEnvironmentParam + +__all__ = ["ScenarioCreateParams"] + + +class ScenarioCreateParams(TypedDict, total=False): + input_context: Required[InputContextParam] + """The input context for the Scenario.""" + + name: Required[str] + """Name of the scenario.""" + + scoring_contract: Required[ScoringContractParam] + """The scoring contract for the Scenario.""" + + environment_parameters: Optional[ScenarioEnvironmentParam] + """The Environment in which the Scenario will run.""" + + metadata: Optional[Dict[str, str]] + """User defined metadata to attach to the scenario for organization.""" + + reference_output: Optional[str] + """A string representation of the reference output to solve the scenario. + + Commonly can be the result of a git diff or a sequence of command actions to + apply to the environment. + """ + + required_environment_variables: Optional[SequenceNotStr[str]] + """Environment variables required to run the scenario. + + If these variables are not provided, the scenario will fail to start. + """ + + required_secret_names: Optional[SequenceNotStr[str]] + """ + Secrets required to run the scenario (user secret name to scenario required + secret name). If these secrets are not provided or the mapping is incorrect, the + scenario will fail to start. + """ + + scorer_timeout_sec: Optional[int] + """Timeout for scoring in seconds. Default 30 minutes (1800s).""" + + validation_type: Optional[Literal["UNSPECIFIED", "FORWARD", "REVERSE", "EVALUATION"]] + """Validation strategy.""" diff --git a/src/runloop_api_client/types/scenario_definition_list_view.py b/src/runloop_api_client/types/scenario_definition_list_view.py new file mode 100644 index 000000000..f39cf1ed9 --- /dev/null +++ b/src/runloop_api_client/types/scenario_definition_list_view.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel +from .scenario_view import ScenarioView + +__all__ = ["ScenarioDefinitionListView"] + + +class ScenarioDefinitionListView(BaseModel): + has_more: bool + + scenarios: List[ScenarioView] + """List of Scenarios matching filter.""" + + remaining_count: Optional[int] = None + + total_count: Optional[int] = None diff --git a/src/runloop_api_client/types/scenario_environment.py b/src/runloop_api_client/types/scenario_environment.py new file mode 100644 index 000000000..b6ac9b039 --- /dev/null +++ b/src/runloop_api_client/types/scenario_environment.py @@ -0,0 +1,29 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel +from .shared.launch_parameters import LaunchParameters + +__all__ = ["ScenarioEnvironment"] + + +class ScenarioEnvironment(BaseModel): + """ + ScenarioEnvironmentParameters specify the environment in which a Scenario will be run. + """ + + blueprint_id: Optional[str] = None + """Use the blueprint with matching ID.""" + + launch_parameters: Optional[LaunchParameters] = None + """Optional launch parameters to apply to the devbox environment at launch.""" + + snapshot_id: Optional[str] = None + """Use the snapshot with matching ID.""" + + working_directory: Optional[str] = None + """The working directory where the agent is expected to fulfill the scenario. + + Scoring functions also run from the working directory. + """ diff --git a/src/runloop_api_client/types/scenario_environment_param.py b/src/runloop_api_client/types/scenario_environment_param.py new file mode 100644 index 000000000..6a219d250 --- /dev/null +++ b/src/runloop_api_client/types/scenario_environment_param.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 Optional +from typing_extensions import TypedDict + +from .shared_params.launch_parameters import LaunchParameters + +__all__ = ["ScenarioEnvironmentParam"] + + +class ScenarioEnvironmentParam(TypedDict, total=False): + """ + ScenarioEnvironmentParameters specify the environment in which a Scenario will be run. + """ + + blueprint_id: Optional[str] + """Use the blueprint with matching ID.""" + + launch_parameters: Optional[LaunchParameters] + """Optional launch parameters to apply to the devbox environment at launch.""" + + snapshot_id: Optional[str] + """Use the snapshot with matching ID.""" + + working_directory: Optional[str] + """The working directory where the agent is expected to fulfill the scenario. + + Scoring functions also run from the working directory. + """ diff --git a/src/runloop_api_client/types/scenario_list_params.py b/src/runloop_api_client/types/scenario_list_params.py new file mode 100644 index 000000000..45ff3a87b --- /dev/null +++ b/src/runloop_api_client/types/scenario_list_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 TypedDict + +__all__ = ["ScenarioListParams"] + + +class ScenarioListParams(TypedDict, total=False): + benchmark_id: str + """Filter scenarios by benchmark ID.""" + + limit: int + """The limit of items to return. Default is 20. Max is 5000.""" + + name: str + """Query for Scenarios with a given name.""" + + starting_after: str + """Load the next page of data starting after the item with the given ID.""" + + validation_type: str + """Filter by validation type""" diff --git a/src/runloop_api_client/types/scenario_list_public_params.py b/src/runloop_api_client/types/scenario_list_public_params.py new file mode 100644 index 000000000..be7e40b8d --- /dev/null +++ b/src/runloop_api_client/types/scenario_list_public_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__ = ["ScenarioListPublicParams"] + + +class ScenarioListPublicParams(TypedDict, total=False): + limit: int + """The limit of items to return. Default is 20. Max is 5000.""" + + name: str + """Query for Scenarios with a given name.""" + + starting_after: str + """Load the next page of data starting after the item with the given ID.""" diff --git a/src/runloop_api_client/types/scenario_run_list_view.py b/src/runloop_api_client/types/scenario_run_list_view.py new file mode 100644 index 000000000..142292dda --- /dev/null +++ b/src/runloop_api_client/types/scenario_run_list_view.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel +from .scenario_run_view import ScenarioRunView + +__all__ = ["ScenarioRunListView"] + + +class ScenarioRunListView(BaseModel): + has_more: bool + + runs: List[ScenarioRunView] + """List of ScenarioRuns matching filter.""" + + remaining_count: Optional[int] = None + + total_count: Optional[int] = None diff --git a/src/runloop_api_client/types/scenario_run_view.py b/src/runloop_api_client/types/scenario_run_view.py new file mode 100644 index 000000000..68d4c3573 --- /dev/null +++ b/src/runloop_api_client/types/scenario_run_view.py @@ -0,0 +1,55 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, Optional +from typing_extensions import Literal + +from .._models import BaseModel +from .scoring_contract_result_view import ScoringContractResultView + +__all__ = ["ScenarioRunView"] + + +class ScenarioRunView(BaseModel): + """A ScenarioRunView represents a single run of a Scenario on a Devbox. + + When completed, the ScenarioRun will contain the final score and output of the run. + """ + + id: str + """ID of the ScenarioRun.""" + + devbox_id: str + """ID of the Devbox on which the Scenario is running.""" + + metadata: Dict[str, str] + """User defined metadata to attach to the scenario run for organization.""" + + scenario_id: str + """ID of the Scenario that has been run.""" + + state: Literal["running", "scoring", "scored", "completed", "canceled", "timeout", "failed"] + """The state of the ScenarioRun.""" + + benchmark_run_id: Optional[str] = None + """ID of the BenchmarkRun that this Scenario is associated with, if any.""" + + duration_ms: Optional[int] = None + """Duration scenario took to run.""" + + environment_variables: Optional[Dict[str, str]] = None + """Environment variables used to run the scenario.""" + + name: Optional[str] = None + """Optional name of ScenarioRun.""" + + purpose: Optional[str] = None + """Purpose of the ScenarioRun.""" + + scoring_contract_result: Optional[ScoringContractResultView] = None + """The scoring result of the ScenarioRun.""" + + secrets_provided: Optional[Dict[str, str]] = None + """User secrets used to run the scenario.""" + + start_time_ms: Optional[int] = None + """The time that the scenario started""" diff --git a/src/runloop_api_client/types/scenario_start_run_params.py b/src/runloop_api_client/types/scenario_start_run_params.py new file mode 100644 index 000000000..6f5e39c00 --- /dev/null +++ b/src/runloop_api_client/types/scenario_start_run_params.py @@ -0,0 +1,28 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, Optional +from typing_extensions import Required, Annotated, TypedDict + +from .._utils import PropertyInfo +from .shared_params.run_profile import RunProfile + +__all__ = ["ScenarioStartRunParams"] + + +class ScenarioStartRunParams(TypedDict, total=False): + scenario_id: Required[str] + """ID of the Scenario to run.""" + + benchmark_run_id: Optional[str] + """Benchmark to associate the run.""" + + metadata: Optional[Dict[str, str]] + """User defined metadata to attach to the run for organization.""" + + run_name: Optional[str] + """Display name of the run.""" + + run_profile: Annotated[Optional[RunProfile], PropertyInfo(alias="runProfile")] + """Runtime configuration to use for this benchmark run""" diff --git a/src/runloop_api_client/types/scenario_update_params.py b/src/runloop_api_client/types/scenario_update_params.py new file mode 100644 index 000000000..c79cb1721 --- /dev/null +++ b/src/runloop_api_client/types/scenario_update_params.py @@ -0,0 +1,52 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, Optional +from typing_extensions import Literal, TypedDict + +from .._types import SequenceNotStr +from .input_context_update_param import InputContextUpdateParam +from .scenario_environment_param import ScenarioEnvironmentParam +from .scoring_contract_update_param import ScoringContractUpdateParam + +__all__ = ["ScenarioUpdateParams"] + + +class ScenarioUpdateParams(TypedDict, total=False): + environment_parameters: Optional[ScenarioEnvironmentParam] + """The Environment in which the Scenario will run.""" + + input_context: Optional[InputContextUpdateParam] + """The input context for the Scenario.""" + + metadata: Optional[Dict[str, str]] + """User defined metadata to attach to the scenario. Pass in empty map to clear.""" + + name: Optional[str] + """Name of the scenario. Cannot be blank.""" + + reference_output: Optional[str] + """A string representation of the reference output to solve the scenario. + + Commonly can be the result of a git diff or a sequence of command actions to + apply to the environment. Pass in empty string to clear. + """ + + required_environment_variables: Optional[SequenceNotStr[str]] + """Environment variables required to run the scenario. + + Pass in empty list to clear. + """ + + required_secret_names: Optional[SequenceNotStr[str]] + """Secrets required to run the scenario. Pass in empty list to clear.""" + + scorer_timeout_sec: Optional[int] + """Timeout for scoring in seconds. Default 30 minutes (1800s).""" + + scoring_contract: Optional[ScoringContractUpdateParam] + """The scoring contract for the Scenario.""" + + validation_type: Optional[Literal["UNSPECIFIED", "FORWARD", "REVERSE", "EVALUATION"]] + """Validation strategy. Pass in empty string to clear.""" diff --git a/src/runloop_api_client/types/scenario_view.py b/src/runloop_api_client/types/scenario_view.py new file mode 100644 index 000000000..3bb470708 --- /dev/null +++ b/src/runloop_api_client/types/scenario_view.py @@ -0,0 +1,70 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, List, Optional +from typing_extensions import Literal + +from .._models import BaseModel +from .input_context import InputContext +from .scoring_contract import ScoringContract +from .scenario_environment import ScenarioEnvironment + +__all__ = ["ScenarioView"] + + +class ScenarioView(BaseModel): + """ + A ScenarioDefinitionView represents a repeatable AI coding evaluation test, complete with initial environment and scoring contract. + """ + + id: str + """The ID of the Scenario.""" + + input_context: InputContext + """The input context for the Scenario.""" + + metadata: Dict[str, str] + """User defined metadata to attach to the scenario for organization.""" + + name: str + """The name of the Scenario.""" + + scoring_contract: ScoringContract + """The scoring contract for the Scenario.""" + + status: Literal["active", "archived"] + """Whether the scenario is active or archived. + + Archived scenarios are excluded from listings and cannot be updated. + """ + + environment: Optional[ScenarioEnvironment] = None + """The Environment in which the Scenario is run.""" + + is_public: Optional[bool] = None + """Whether this scenario is public.""" + + reference_output: Optional[str] = None + """A string representation of the reference output to solve the scenario. + + Commonly can be the result of a git diff or a sequence of command actions to + apply to the environment. + """ + + required_environment_variables: Optional[List[str]] = None + """Environment variables required to run the scenario. + + If any required environment variables are missing, the scenario will fail to + start. + """ + + required_secret_names: Optional[List[str]] = None + """Environment variables required to run the scenario. + + If any required secrets are missing, the scenario will fail to start. + """ + + scorer_timeout_sec: Optional[int] = None + """Timeout for scoring in seconds. Default 30 minutes (1800s).""" + + validation_type: Optional[Literal["UNSPECIFIED", "FORWARD", "REVERSE", "EVALUATION"]] = None + """Validation strategy.""" diff --git a/src/runloop_api_client/types/scenarios/__init__.py b/src/runloop_api_client/types/scenarios/__init__.py new file mode 100644 index 000000000..97efebd9d --- /dev/null +++ b/src/runloop_api_client/types/scenarios/__init__.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .run_list_params import RunListParams as RunListParams +from .scorer_list_params import ScorerListParams as ScorerListParams +from .scorer_create_params import ScorerCreateParams as ScorerCreateParams +from .scorer_list_response import ScorerListResponse as ScorerListResponse +from .scorer_update_params import ScorerUpdateParams as ScorerUpdateParams +from .scorer_create_response import ScorerCreateResponse as ScorerCreateResponse +from .scorer_update_response import ScorerUpdateResponse as ScorerUpdateResponse +from .scorer_retrieve_response import ScorerRetrieveResponse as ScorerRetrieveResponse diff --git a/src/runloop_api_client/types/scenarios/run_list_params.py b/src/runloop_api_client/types/scenarios/run_list_params.py new file mode 100644 index 000000000..97eeb425a --- /dev/null +++ b/src/runloop_api_client/types/scenarios/run_list_params.py @@ -0,0 +1,27 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["RunListParams"] + + +class RunListParams(TypedDict, total=False): + benchmark_run_id: str + """Filter by benchmark run ID""" + + limit: int + """The limit of items to return. Default is 20. Max is 5000.""" + + name: str + """Filter by name""" + + scenario_id: str + """Filter runs associated to Scenario given ID""" + + starting_after: str + """Load the next page of data starting after the item with the given ID.""" + + state: str + """Filter by state""" diff --git a/src/runloop_api_client/types/scenarios/scorer_create_params.py b/src/runloop_api_client/types/scenarios/scorer_create_params.py new file mode 100644 index 000000000..62a4e33f9 --- /dev/null +++ b/src/runloop_api_client/types/scenarios/scorer_create_params.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["ScorerCreateParams"] + + +class ScorerCreateParams(TypedDict, total=False): + bash_script: Required[str] + """ + Bash script for the custom scorer taking context as a json object + $RL_SCORER_CONTEXT. + """ + + type: Required[str] + """Name of the type of custom scorer.""" diff --git a/src/runloop_api_client/types/scenarios/scorer_create_response.py b/src/runloop_api_client/types/scenarios/scorer_create_response.py new file mode 100644 index 000000000..2b6e665a1 --- /dev/null +++ b/src/runloop_api_client/types/scenarios/scorer_create_response.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from ..._models import BaseModel + +__all__ = ["ScorerCreateResponse"] + + +class ScorerCreateResponse(BaseModel): + """A ScenarioScorerView represents a custom scoring function for a Scenario.""" + + id: str + """ID for the scenario scorer.""" + + bash_script: str + """Bash script that takes in $RL_SCORER_CONTEXT as env variable and runs scoring.""" + + type: str + """Name of the type of scenario scorer.""" diff --git a/src/runloop_api_client/types/scenarios/scorer_list_params.py b/src/runloop_api_client/types/scenarios/scorer_list_params.py new file mode 100644 index 000000000..f80e7f6ac --- /dev/null +++ b/src/runloop_api_client/types/scenarios/scorer_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__ = ["ScorerListParams"] + + +class ScorerListParams(TypedDict, total=False): + limit: int + """The limit of items to return. Default is 20. Max is 5000.""" + + starting_after: str + """Load the next page of data starting after the item with the given ID.""" diff --git a/src/runloop_api_client/types/scenarios/scorer_list_response.py b/src/runloop_api_client/types/scenarios/scorer_list_response.py new file mode 100644 index 000000000..46eb8802e --- /dev/null +++ b/src/runloop_api_client/types/scenarios/scorer_list_response.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from ..._models import BaseModel + +__all__ = ["ScorerListResponse"] + + +class ScorerListResponse(BaseModel): + """A ScenarioScorerView represents a custom scoring function for a Scenario.""" + + id: str + """ID for the scenario scorer.""" + + bash_script: str + """Bash script that takes in $RL_SCORER_CONTEXT as env variable and runs scoring.""" + + type: str + """Name of the type of scenario scorer.""" diff --git a/src/runloop_api_client/types/scenarios/scorer_retrieve_response.py b/src/runloop_api_client/types/scenarios/scorer_retrieve_response.py new file mode 100644 index 000000000..a67cd35c0 --- /dev/null +++ b/src/runloop_api_client/types/scenarios/scorer_retrieve_response.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from ..._models import BaseModel + +__all__ = ["ScorerRetrieveResponse"] + + +class ScorerRetrieveResponse(BaseModel): + """A ScenarioScorerView represents a custom scoring function for a Scenario.""" + + id: str + """ID for the scenario scorer.""" + + bash_script: str + """Bash script that takes in $RL_SCORER_CONTEXT as env variable and runs scoring.""" + + type: str + """Name of the type of scenario scorer.""" diff --git a/src/runloop_api_client/types/scenarios/scorer_update_params.py b/src/runloop_api_client/types/scenarios/scorer_update_params.py new file mode 100644 index 000000000..dcc7816a4 --- /dev/null +++ b/src/runloop_api_client/types/scenarios/scorer_update_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__ = ["ScorerUpdateParams"] + + +class ScorerUpdateParams(TypedDict, total=False): + bash_script: Required[str] + """ + Bash script for the custom scorer taking context as a json object + $RL_SCORER_CONTEXT. + """ + + type: Required[str] + """Name of the type of custom scorer.""" diff --git a/src/runloop_api_client/types/scenarios/scorer_update_response.py b/src/runloop_api_client/types/scenarios/scorer_update_response.py new file mode 100644 index 000000000..91e668d22 --- /dev/null +++ b/src/runloop_api_client/types/scenarios/scorer_update_response.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from ..._models import BaseModel + +__all__ = ["ScorerUpdateResponse"] + + +class ScorerUpdateResponse(BaseModel): + """A ScenarioScorerView represents a custom scoring function for a Scenario.""" + + id: str + """ID for the scenario scorer.""" + + bash_script: str + """Bash script that takes in $RL_SCORER_CONTEXT as env variable and runs scoring.""" + + type: str + """Name of the type of scenario scorer.""" diff --git a/src/runloop_api_client/types/scoring_contract.py b/src/runloop_api_client/types/scoring_contract.py new file mode 100644 index 000000000..d3c646fda --- /dev/null +++ b/src/runloop_api_client/types/scoring_contract.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List + +from .._models import BaseModel +from .scoring_function import ScoringFunction + +__all__ = ["ScoringContract"] + + +class ScoringContract(BaseModel): + """ + InputContextView specifies the problem statement along with all additional context for a Scenario. + """ + + scoring_function_parameters: List[ScoringFunction] + """A list of scoring functions used to evaluate the Scenario.""" diff --git a/src/runloop_api_client/types/scoring_contract_param.py b/src/runloop_api_client/types/scoring_contract_param.py new file mode 100644 index 000000000..46f1b7b6d --- /dev/null +++ b/src/runloop_api_client/types/scoring_contract_param.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 Iterable +from typing_extensions import Required, TypedDict + +from .scoring_function_param import ScoringFunctionParam + +__all__ = ["ScoringContractParam"] + + +class ScoringContractParam(TypedDict, total=False): + """ + InputContextView specifies the problem statement along with all additional context for a Scenario. + """ + + scoring_function_parameters: Required[Iterable[ScoringFunctionParam]] + """A list of scoring functions used to evaluate the Scenario.""" diff --git a/src/runloop_api_client/types/scoring_contract_result_view.py b/src/runloop_api_client/types/scoring_contract_result_view.py new file mode 100644 index 000000000..85e1a42c4 --- /dev/null +++ b/src/runloop_api_client/types/scoring_contract_result_view.py @@ -0,0 +1,20 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List + +from .._models import BaseModel +from .scoring_function_result_view import ScoringFunctionResultView + +__all__ = ["ScoringContractResultView"] + + +class ScoringContractResultView(BaseModel): + """ + A ScoringContractResultView represents the result of running all scoring functions on a given input context. + """ + + score: float + """Total score for all scoring contracts. This will be a value between 0 and 1.""" + + scoring_function_results: List[ScoringFunctionResultView] + """List of all individual scoring function results.""" diff --git a/src/runloop_api_client/types/scoring_contract_update_param.py b/src/runloop_api_client/types/scoring_contract_update_param.py new file mode 100644 index 000000000..ce2c85289 --- /dev/null +++ b/src/runloop_api_client/types/scoring_contract_update_param.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Iterable, Optional +from typing_extensions import TypedDict + +from .scoring_function_param import ScoringFunctionParam + +__all__ = ["ScoringContractUpdateParam"] + + +class ScoringContractUpdateParam(TypedDict, total=False): + scoring_function_parameters: Optional[Iterable[ScoringFunctionParam]] + """A list of scoring functions used to evaluate the Scenario.""" diff --git a/src/runloop_api_client/types/scoring_function.py b/src/runloop_api_client/types/scoring_function.py new file mode 100644 index 000000000..837a46024 --- /dev/null +++ b/src/runloop_api_client/types/scoring_function.py @@ -0,0 +1,157 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Union, Optional +from typing_extensions import Literal, Annotated, TypeAlias + +from .._utils import PropertyInfo +from .._models import BaseModel + +__all__ = [ + "ScoringFunction", + "Scorer", + "ScorerAstGrepScoringFunction", + "ScorerBashScriptScoringFunction", + "ScorerCommandScoringFunction", + "ScorerCustomScoringFunction", + "ScorerPythonScriptScoringFunction", + "ScorerTestBasedScoringFunction", + "ScorerTestBasedScoringFunctionTestFile", +] + + +class ScorerAstGrepScoringFunction(BaseModel): + """AstGrepScoringFunction utilizes structured coach search for scoring.""" + + pattern: str + """AST pattern to match. + + Pattern will be passed to ast-grep using the commandline surround by double + quotes ("), so make sure to use proper escaping (for example, \\$$\\$$\\$$). + """ + + search_directory: str + """The path to search.""" + + type: Literal["ast_grep_scorer"] + + lang: Optional[str] = None + """The language of the pattern.""" + + +class ScorerBashScriptScoringFunction(BaseModel): + """ + BashScriptScoringFunction is a scoring function specified by a bash script that will be run in the context of your environment. + """ + + type: Literal["bash_script_scorer"] + + bash_script: Optional[str] = None + """ + A single bash script that sets up the environment, scores, and prints the final + score to standard out. Score should be a float between 0.0 and 1.0, and look + like "score=[0.0..1.0]. + """ + + +class ScorerCommandScoringFunction(BaseModel): + """ + CommandScoringFunction executes a single command and checks the result.The output of the command will be printed. Scoring will passed if the command returns status code 0, otherwise it will be failed. + """ + + type: Literal["command_scorer"] + + command: Optional[str] = None + """The command to execute.""" + + +class ScorerCustomScoringFunction(BaseModel): + """CustomScoringFunction is a custom, user defined scoring function.""" + + custom_scorer_type: str + """Type of the scoring function, previously registered with Runloop.""" + + type: Literal["custom_scorer"] + + scorer_params: Optional[object] = None + """Additional JSON structured context to pass to the scoring function.""" + + +class ScorerPythonScriptScoringFunction(BaseModel): + """ + PythonScriptScoringFunction will run a python script in the context of your environment as a ScoringFunction. + """ + + python_script: str + """Python script to be run. + + The script should output the score to standard out as a float between 0.0 and + 1.0. + """ + + type: Literal["python_script_scorer"] + + python_version_constraint: Optional[str] = None + """Python version to run scoring. Default is "==3.12.10" """ + + requirements_contents: Optional[str] = None + """Package dependencies to be installed. + + The requirements should be a valid requirements.txt file. + """ + + +class ScorerTestBasedScoringFunctionTestFile(BaseModel): + file_contents: Optional[str] = None + """Content of the test file""" + + file_path: Optional[str] = None + """ + Path to write content of the test file, relative to your environment's working + directory + """ + + +class ScorerTestBasedScoringFunction(BaseModel): + """ + TestBasedScoringFunction writes test files to disk and executes a test command to verify the solution. + """ + + type: Literal["test_based_scorer"] + + test_command: Optional[str] = None + """The command to execute for running the tests""" + + test_files: Optional[List[ScorerTestBasedScoringFunctionTestFile]] = None + """List of test files to create""" + + +Scorer: TypeAlias = Annotated[ + Union[ + ScorerAstGrepScoringFunction, + ScorerBashScriptScoringFunction, + ScorerCommandScoringFunction, + ScorerCustomScoringFunction, + ScorerPythonScriptScoringFunction, + ScorerTestBasedScoringFunction, + ], + PropertyInfo(discriminator="type"), +] + + +class ScoringFunction(BaseModel): + """ScoringFunction specifies a method of scoring a Scenario.""" + + name: str + """Name of scoring function. Names must only contain [a-zA-Z0-9_-].""" + + scorer: Scorer + """The scoring function to use for evaluating this scenario. + + The type field determines which built-in function to use. + """ + + weight: float + """Weight to apply to scoring function score. + + Weights of all scoring functions should sum to 1.0. + """ diff --git a/src/runloop_api_client/types/scoring_function_param.py b/src/runloop_api_client/types/scoring_function_param.py new file mode 100644 index 000000000..c619c8d91 --- /dev/null +++ b/src/runloop_api_client/types/scoring_function_param.py @@ -0,0 +1,153 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union, Iterable, Optional +from typing_extensions import Literal, Required, TypeAlias, TypedDict + +__all__ = [ + "ScoringFunctionParam", + "Scorer", + "ScorerAstGrepScoringFunction", + "ScorerBashScriptScoringFunction", + "ScorerCommandScoringFunction", + "ScorerCustomScoringFunction", + "ScorerPythonScriptScoringFunction", + "ScorerTestBasedScoringFunction", + "ScorerTestBasedScoringFunctionTestFile", +] + + +class ScorerAstGrepScoringFunction(TypedDict, total=False): + """AstGrepScoringFunction utilizes structured coach search for scoring.""" + + pattern: Required[str] + """AST pattern to match. + + Pattern will be passed to ast-grep using the commandline surround by double + quotes ("), so make sure to use proper escaping (for example, \\$$\\$$\\$$). + """ + + search_directory: Required[str] + """The path to search.""" + + type: Required[Literal["ast_grep_scorer"]] + + lang: str + """The language of the pattern.""" + + +class ScorerBashScriptScoringFunction(TypedDict, total=False): + """ + BashScriptScoringFunction is a scoring function specified by a bash script that will be run in the context of your environment. + """ + + type: Required[Literal["bash_script_scorer"]] + + bash_script: str + """ + A single bash script that sets up the environment, scores, and prints the final + score to standard out. Score should be a float between 0.0 and 1.0, and look + like "score=[0.0..1.0]. + """ + + +class ScorerCommandScoringFunction(TypedDict, total=False): + """ + CommandScoringFunction executes a single command and checks the result.The output of the command will be printed. Scoring will passed if the command returns status code 0, otherwise it will be failed. + """ + + type: Required[Literal["command_scorer"]] + + command: str + """The command to execute.""" + + +class ScorerCustomScoringFunction(TypedDict, total=False): + """CustomScoringFunction is a custom, user defined scoring function.""" + + custom_scorer_type: Required[str] + """Type of the scoring function, previously registered with Runloop.""" + + type: Required[Literal["custom_scorer"]] + + scorer_params: Optional[object] + """Additional JSON structured context to pass to the scoring function.""" + + +class ScorerPythonScriptScoringFunction(TypedDict, total=False): + """ + PythonScriptScoringFunction will run a python script in the context of your environment as a ScoringFunction. + """ + + python_script: Required[str] + """Python script to be run. + + The script should output the score to standard out as a float between 0.0 and + 1.0. + """ + + type: Required[Literal["python_script_scorer"]] + + python_version_constraint: Optional[str] + """Python version to run scoring. Default is "==3.12.10" """ + + requirements_contents: Optional[str] + """Package dependencies to be installed. + + The requirements should be a valid requirements.txt file. + """ + + +class ScorerTestBasedScoringFunctionTestFile(TypedDict, total=False): + file_contents: str + """Content of the test file""" + + file_path: str + """ + Path to write content of the test file, relative to your environment's working + directory + """ + + +class ScorerTestBasedScoringFunction(TypedDict, total=False): + """ + TestBasedScoringFunction writes test files to disk and executes a test command to verify the solution. + """ + + type: Required[Literal["test_based_scorer"]] + + test_command: str + """The command to execute for running the tests""" + + test_files: Iterable[ScorerTestBasedScoringFunctionTestFile] + """List of test files to create""" + + +Scorer: TypeAlias = Union[ + ScorerAstGrepScoringFunction, + ScorerBashScriptScoringFunction, + ScorerCommandScoringFunction, + ScorerCustomScoringFunction, + ScorerPythonScriptScoringFunction, + ScorerTestBasedScoringFunction, +] + + +class ScoringFunctionParam(TypedDict, total=False): + """ScoringFunction specifies a method of scoring a Scenario.""" + + name: Required[str] + """Name of scoring function. Names must only contain [a-zA-Z0-9_-].""" + + scorer: Required[Scorer] + """The scoring function to use for evaluating this scenario. + + The type field determines which built-in function to use. + """ + + weight: Required[float] + """Weight to apply to scoring function score. + + Weights of all scoring functions should sum to 1.0. + """ diff --git a/src/runloop_api_client/types/scoring_function_result_view.py b/src/runloop_api_client/types/scoring_function_result_view.py new file mode 100644 index 000000000..4fe5b67cb --- /dev/null +++ b/src/runloop_api_client/types/scoring_function_result_view.py @@ -0,0 +1,25 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["ScoringFunctionResultView"] + + +class ScoringFunctionResultView(BaseModel): + """ + A ScoringFunctionResultView represents the result of running a single scoring function on a given input context. + """ + + output: str + """Log output of the scoring function.""" + + score: float + """Final score for the given scoring function.""" + + scoring_function_name: str + """Scoring function name that ran.""" + + state: Literal["unknown", "complete", "error"] + """The state of the scoring function application.""" diff --git a/src/runloop_api_client/types/secret_create_params.py b/src/runloop_api_client/types/secret_create_params.py new file mode 100644 index 000000000..5d5234b56 --- /dev/null +++ b/src/runloop_api_client/types/secret_create_params.py @@ -0,0 +1,23 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["SecretCreateParams"] + + +class SecretCreateParams(TypedDict, total=False): + name: Required[str] + """The globally unique name for the Secret. + + Must be a valid environment variable name (alphanumeric and underscores only). + Example: 'DATABASE_PASSWORD' + """ + + value: Required[str] + """The value to store for this Secret. + + This will be encrypted at rest and made available as an environment variable in + Devboxes. Example: 'my-secure-password' + """ diff --git a/src/runloop_api_client/types/secret_list_params.py b/src/runloop_api_client/types/secret_list_params.py new file mode 100644 index 000000000..13d25bd7e --- /dev/null +++ b/src/runloop_api_client/types/secret_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__ = ["SecretListParams"] + + +class SecretListParams(TypedDict, total=False): + limit: int + """The limit of items to return. Default is 20. Max is 5000.""" diff --git a/src/runloop_api_client/types/secret_list_view.py b/src/runloop_api_client/types/secret_list_view.py new file mode 100644 index 000000000..2f10bae2c --- /dev/null +++ b/src/runloop_api_client/types/secret_list_view.py @@ -0,0 +1,30 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel +from .secret_view import SecretView + +__all__ = ["SecretListView"] + + +class SecretListView(BaseModel): + """A paginated list of Secrets.""" + + has_more: bool + """True if there are more results available beyond this page.""" + + secrets: List[SecretView] + """List of Secret objects. Values are omitted for security.""" + + remaining_count: Optional[int] = None + """Number of Secrets remaining after this page. + + Deprecated: will be removed in a future breaking change. + """ + + total_count: Optional[int] = None + """Total number of Secrets across all pages. + + Deprecated: will be removed in a future breaking change. + """ diff --git a/src/runloop_api_client/types/secret_update_params.py b/src/runloop_api_client/types/secret_update_params.py new file mode 100644 index 000000000..5b5af9a0f --- /dev/null +++ b/src/runloop_api_client/types/secret_update_params.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["SecretUpdateParams"] + + +class SecretUpdateParams(TypedDict, total=False): + value: Required[str] + """The new value for the Secret. + + This will replace the existing value and be encrypted at rest. Example: + 'my-updated-secure-password' + """ diff --git a/src/runloop_api_client/types/secret_view.py b/src/runloop_api_client/types/secret_view.py new file mode 100644 index 000000000..bd1c8811e --- /dev/null +++ b/src/runloop_api_client/types/secret_view.py @@ -0,0 +1,26 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["SecretView"] + + +class SecretView(BaseModel): + """ + A Secret represents a key-value pair that can be securely stored and used in Devboxes as environment variables. + """ + + id: str + """The unique identifier of the Secret.""" + + create_time_ms: int + """Creation time of the Secret (Unix timestamp in milliseconds).""" + + name: str + """The globally unique name of the Secret. + + Used as the environment variable name in Devboxes. + """ + + update_time_ms: int + """Last update time of the Secret (Unix timestamp in milliseconds).""" diff --git a/src/runloop_api_client/types/shared/__init__.py b/src/runloop_api_client/types/shared/__init__.py new file mode 100644 index 000000000..ab07ef757 --- /dev/null +++ b/src/runloop_api_client/types/shared/__init__.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .mount import Mount as Mount +from .after_idle import AfterIdle as AfterIdle +from .agent_mount import AgentMount as AgentMount +from .run_profile import RunProfile as RunProfile +from .agent_source import AgentSource as AgentSource +from .object_mount import ObjectMount as ObjectMount +from .launch_parameters import LaunchParameters as LaunchParameters +from .code_mount_parameters import CodeMountParameters as CodeMountParameters diff --git a/src/runloop_api_client/types/shared/after_idle.py b/src/runloop_api_client/types/shared/after_idle.py new file mode 100644 index 000000000..b9511d7f2 --- /dev/null +++ b/src/runloop_api_client/types/shared/after_idle.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["AfterIdle"] + + +class AfterIdle(BaseModel): + idle_time_seconds: int + """After idle_time_seconds, on_idle action will be taken.""" + + on_idle: Literal["shutdown", "suspend"] + """Action to take after Devbox becomes idle.""" diff --git a/src/runloop_api_client/types/shared/agent_mount.py b/src/runloop_api_client/types/shared/agent_mount.py new file mode 100644 index 000000000..cdb196dd5 --- /dev/null +++ b/src/runloop_api_client/types/shared/agent_mount.py @@ -0,0 +1,31 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["AgentMount"] + + +class AgentMount(BaseModel): + agent_id: Optional[str] = None + """The ID of the agent to mount. Either agent_id or name must be set.""" + + agent_name: Optional[str] = None + """The name of the agent to mount. + + Returns the most recent agent with a matching name if no agent id string + provided. Either agent id or name must be set + """ + + type: Literal["agent_mount"] + + agent_path: Optional[str] = None + """Path to mount the agent on the Devbox. + + Required for git and object agents. Use absolute path (e.g., /home/user/agent) + """ + + auth_token: Optional[str] = None + """Optional auth token for private repositories. Only used for git agents.""" diff --git a/src/runloop_api_client/types/shared/agent_source.py b/src/runloop_api_client/types/shared/agent_source.py new file mode 100644 index 000000000..9282d6181 --- /dev/null +++ b/src/runloop_api_client/types/shared/agent_source.py @@ -0,0 +1,75 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from ..._models import BaseModel + +__all__ = ["AgentSource", "Git", "Npm", "Object", "Pip"] + + +class Git(BaseModel): + """Git source configuration""" + + repository: str + """Git repository URL""" + + agent_setup: Optional[List[str]] = None + """Setup commands to run after cloning""" + + ref: Optional[str] = None + """Optional Git ref (branch/tag/commit), defaults to main/HEAD""" + + +class Npm(BaseModel): + """NPM source configuration""" + + package_name: str + """NPM package name""" + + agent_setup: Optional[List[str]] = None + """Setup commands to run after installation""" + + registry_url: Optional[str] = None + """NPM registry URL""" + + +class Object(BaseModel): + """Object store source configuration""" + + object_id: str + """Object ID""" + + agent_setup: Optional[List[str]] = None + """Setup commands to run after unpacking""" + + +class Pip(BaseModel): + """Pip source configuration""" + + package_name: str + """Pip package name""" + + agent_setup: Optional[List[str]] = None + """Setup commands to run after installation""" + + registry_url: Optional[str] = None + """Pip registry URL""" + + +class AgentSource(BaseModel): + """Agent source configuration.""" + + type: str + """Source type: npm, pip, object, or git""" + + git: Optional[Git] = None + """Git source configuration""" + + npm: Optional[Npm] = None + """NPM source configuration""" + + object: Optional[Object] = None + """Object store source configuration""" + + pip: Optional[Pip] = None + """Pip source configuration""" diff --git a/src/runloop_api_client/types/shared/code_mount_parameters.py b/src/runloop_api_client/types/shared/code_mount_parameters.py new file mode 100644 index 000000000..b430f1513 --- /dev/null +++ b/src/runloop_api_client/types/shared/code_mount_parameters.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__ = ["CodeMountParameters"] + + +class CodeMountParameters(BaseModel): + repo_name: str + """The name of the repo to mount. + + By default, code will be mounted at /home/user/{repo_name}s. + """ + + repo_owner: str + """The owner of the repo.""" + + token: Optional[str] = None + """The authentication token necessary to pull repo.""" + + install_command: Optional[str] = None + """Installation command to install and setup repository.""" diff --git a/src/runloop_api_client/types/shared/launch_parameters.py b/src/runloop_api_client/types/shared/launch_parameters.py new file mode 100644 index 000000000..0264fa5c8 --- /dev/null +++ b/src/runloop_api_client/types/shared/launch_parameters.py @@ -0,0 +1,86 @@ +# 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 .after_idle import AfterIdle + +__all__ = ["LaunchParameters", "UserParameters"] + + +class UserParameters(BaseModel): + """Specify the user for execution on Devbox. + + If not set, default `user` will be used. + """ + + uid: int + """User ID (UID) for the Linux user. Must be a non-negative integer.""" + + username: str + """Username for the Linux user.""" + + +class LaunchParameters(BaseModel): + """ + LaunchParameters enable you to customize the resources available to your Devbox as well as the environment set up that should be completed before the Devbox is marked as 'running'. + """ + + after_idle: Optional[AfterIdle] = None + """Configure Devbox lifecycle based on idle activity. + + If after_idle is set, Devbox will ignore keep_alive_time_seconds. + """ + + architecture: Optional[Literal["x86_64", "arm64"]] = None + """The target architecture for the Devbox. If unset, defaults to x86_64.""" + + available_ports: Optional[List[int]] = None + """A list of ports to make available on the Devbox. + + Only ports made available will be surfaced to create tunnels via the + 'createTunnel' API. + """ + + custom_cpu_cores: Optional[int] = None + """Custom CPU cores. Must be 0.5, 1, or a multiple of 2. Max is 16.""" + + custom_disk_size: Optional[int] = None + """Custom disk size in GiB. Must be a multiple of 2. Min is 2GiB, max is 64GiB.""" + + custom_gb_memory: Optional[int] = None + """Custom memory size in GiB. Must be 1 or a multiple of 2. Max is 64GiB.""" + + keep_alive_time_seconds: Optional[int] = None + """Time in seconds after which Devbox will automatically shutdown. + + Default is 1 hour. Maximum is 48 hours (172800 seconds). + """ + + launch_commands: Optional[List[str]] = None + """Set of commands to be run at launch time, before the entrypoint process is run.""" + + network_policy_id: Optional[str] = None + """ + (Optional) ID of the network policy to apply to Devboxes launched with these + parameters. When set on a Blueprint launch parameters, Devboxes created from it + will inherit this policy unless explicitly overridden. + """ + + required_services: Optional[List[str]] = None + """A list of ContainerizedService names to be started when a Devbox is created. + + A valid ContainerizedService must be specified in Blueprint to be started. + """ + + resource_size_request: Optional[ + Literal["X_SMALL", "SMALL", "MEDIUM", "LARGE", "X_LARGE", "XX_LARGE", "CUSTOM_SIZE"] + ] = None + """Manual resource configuration for Devbox. If not set, defaults will be used.""" + + user_parameters: Optional[UserParameters] = None + """Specify the user for execution on Devbox. + + If not set, default `user` will be used. + """ diff --git a/src/runloop_api_client/types/shared/mount.py b/src/runloop_api_client/types/shared/mount.py new file mode 100644 index 000000000..855748fc4 --- /dev/null +++ b/src/runloop_api_client/types/shared/mount.py @@ -0,0 +1,43 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Union, Optional +from typing_extensions import Literal, Annotated, TypeAlias + +from ..._utils import PropertyInfo +from ..._models import BaseModel +from .agent_mount import AgentMount +from .object_mount import ObjectMount + +__all__ = ["Mount", "CodeMount", "FileMount"] + + +class CodeMount(BaseModel): + repo_name: str + """The name of the repo to mount. + + By default, code will be mounted at /home/user/{repo_name}s. + """ + + repo_owner: str + """The owner of the repo.""" + + type: Literal["code_mount"] + + token: Optional[str] = None + """The authentication token necessary to pull repo.""" + + install_command: Optional[str] = None + """Installation command to install and setup repository.""" + + +class FileMount(BaseModel): + content: str + """Content of the file to mount.""" + + target: str + """Target path where the file should be mounted.""" + + type: Literal["file_mount"] + + +Mount: TypeAlias = Annotated[Union[ObjectMount, AgentMount, CodeMount, FileMount], PropertyInfo(discriminator="type")] diff --git a/src/runloop_api_client/types/shared/object_mount.py b/src/runloop_api_client/types/shared/object_mount.py new file mode 100644 index 000000000..22f1b839b --- /dev/null +++ b/src/runloop_api_client/types/shared/object_mount.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__ = ["ObjectMount"] + + +class ObjectMount(BaseModel): + object_id: str + """The ID of the object to write.""" + + object_path: str + """The path to write the object on the Devbox. + + Use absolute path of object (ie /home/user/object.txt, or directory if archive + /home/user/archive_dir) + """ + + type: Literal["object_mount"] diff --git a/src/runloop_api_client/types/shared/run_profile.py b/src/runloop_api_client/types/shared/run_profile.py new file mode 100644 index 000000000..21cf31f92 --- /dev/null +++ b/src/runloop_api_client/types/shared/run_profile.py @@ -0,0 +1,37 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, List, Optional + +from pydantic import Field as FieldInfo + +from .mount import Mount +from ..._models import BaseModel +from .launch_parameters import LaunchParameters + +__all__ = ["RunProfile"] + + +class RunProfile(BaseModel): + env_vars: Optional[Dict[str, str]] = FieldInfo(alias="envVars", default=None) + """Mapping of Environment Variable to Value. + + May be shown in devbox logging. Example: {"DB_PASS": "DATABASE_PASSWORD"} would + set the environment variable 'DB_PASS' to the value 'DATABASE_PASSWORD_VALUE'. + """ + + launch_parameters: Optional[LaunchParameters] = FieldInfo(alias="launchParameters", default=None) + """Additional runtime LaunchParameters to apply after the devbox starts.""" + + mounts: Optional[List[Mount]] = None + """A list of mounts to be included in the scenario run.""" + + purpose: Optional[str] = None + """Purpose of the run.""" + + secrets: Optional[Dict[str, str]] = None + """Mapping of Environment Variable to User Secret Name. + + Never shown in devbox logging. Example: {"DB_PASS": "DATABASE_PASSWORD"} would + set the environment variable 'DB_PASS' to the value of the secret + 'DATABASE_PASSWORD'. + """ diff --git a/src/runloop_api_client/types/shared_params/__init__.py b/src/runloop_api_client/types/shared_params/__init__.py new file mode 100644 index 000000000..ab07ef757 --- /dev/null +++ b/src/runloop_api_client/types/shared_params/__init__.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .mount import Mount as Mount +from .after_idle import AfterIdle as AfterIdle +from .agent_mount import AgentMount as AgentMount +from .run_profile import RunProfile as RunProfile +from .agent_source import AgentSource as AgentSource +from .object_mount import ObjectMount as ObjectMount +from .launch_parameters import LaunchParameters as LaunchParameters +from .code_mount_parameters import CodeMountParameters as CodeMountParameters diff --git a/src/runloop_api_client/types/shared_params/after_idle.py b/src/runloop_api_client/types/shared_params/after_idle.py new file mode 100644 index 000000000..a9112b6b8 --- /dev/null +++ b/src/runloop_api_client/types/shared_params/after_idle.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__ = ["AfterIdle"] + + +class AfterIdle(TypedDict, total=False): + idle_time_seconds: Required[int] + """After idle_time_seconds, on_idle action will be taken.""" + + on_idle: Required[Literal["shutdown", "suspend"]] + """Action to take after Devbox becomes idle.""" diff --git a/src/runloop_api_client/types/shared_params/agent_mount.py b/src/runloop_api_client/types/shared_params/agent_mount.py new file mode 100644 index 000000000..0c83a5fd7 --- /dev/null +++ b/src/runloop_api_client/types/shared_params/agent_mount.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 Optional +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["AgentMount"] + + +class AgentMount(TypedDict, total=False): + agent_id: Required[Optional[str]] + """The ID of the agent to mount. Either agent_id or name must be set.""" + + agent_name: Required[Optional[str]] + """The name of the agent to mount. + + Returns the most recent agent with a matching name if no agent id string + provided. Either agent id or name must be set + """ + + type: Required[Literal["agent_mount"]] + + agent_path: Optional[str] + """Path to mount the agent on the Devbox. + + Required for git and object agents. Use absolute path (e.g., /home/user/agent) + """ + + auth_token: Optional[str] + """Optional auth token for private repositories. Only used for git agents.""" diff --git a/src/runloop_api_client/types/shared_params/agent_source.py b/src/runloop_api_client/types/shared_params/agent_source.py new file mode 100644 index 000000000..7132414c8 --- /dev/null +++ b/src/runloop_api_client/types/shared_params/agent_source.py @@ -0,0 +1,78 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Required, TypedDict + +from ..._types import SequenceNotStr + +__all__ = ["AgentSource", "Git", "Npm", "Object", "Pip"] + + +class Git(TypedDict, total=False): + """Git source configuration""" + + repository: Required[str] + """Git repository URL""" + + agent_setup: Optional[SequenceNotStr[str]] + """Setup commands to run after cloning""" + + ref: Optional[str] + """Optional Git ref (branch/tag/commit), defaults to main/HEAD""" + + +class Npm(TypedDict, total=False): + """NPM source configuration""" + + package_name: Required[str] + """NPM package name""" + + agent_setup: Optional[SequenceNotStr[str]] + """Setup commands to run after installation""" + + registry_url: Optional[str] + """NPM registry URL""" + + +class Object(TypedDict, total=False): + """Object store source configuration""" + + object_id: Required[str] + """Object ID""" + + agent_setup: Optional[SequenceNotStr[str]] + """Setup commands to run after unpacking""" + + +class Pip(TypedDict, total=False): + """Pip source configuration""" + + package_name: Required[str] + """Pip package name""" + + agent_setup: Optional[SequenceNotStr[str]] + """Setup commands to run after installation""" + + registry_url: Optional[str] + """Pip registry URL""" + + +class AgentSource(TypedDict, total=False): + """Agent source configuration.""" + + type: Required[str] + """Source type: npm, pip, object, or git""" + + git: Optional[Git] + """Git source configuration""" + + npm: Optional[Npm] + """NPM source configuration""" + + object: Optional[Object] + """Object store source configuration""" + + pip: Optional[Pip] + """Pip source configuration""" diff --git a/src/runloop_api_client/types/shared_params/code_mount_parameters.py b/src/runloop_api_client/types/shared_params/code_mount_parameters.py new file mode 100644 index 000000000..5afa72523 --- /dev/null +++ b/src/runloop_api_client/types/shared_params/code_mount_parameters.py @@ -0,0 +1,25 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Required, TypedDict + +__all__ = ["CodeMountParameters"] + + +class CodeMountParameters(TypedDict, total=False): + repo_name: Required[str] + """The name of the repo to mount. + + By default, code will be mounted at /home/user/{repo_name}s. + """ + + repo_owner: Required[str] + """The owner of the repo.""" + + token: Optional[str] + """The authentication token necessary to pull repo.""" + + install_command: Optional[str] + """Installation command to install and setup repository.""" diff --git a/src/runloop_api_client/types/shared_params/launch_parameters.py b/src/runloop_api_client/types/shared_params/launch_parameters.py new file mode 100644 index 000000000..5c785b9f9 --- /dev/null +++ b/src/runloop_api_client/types/shared_params/launch_parameters.py @@ -0,0 +1,88 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Iterable, Optional +from typing_extensions import Literal, Required, TypedDict + +from ..._types import SequenceNotStr +from .after_idle import AfterIdle + +__all__ = ["LaunchParameters", "UserParameters"] + + +class UserParameters(TypedDict, total=False): + """Specify the user for execution on Devbox. + + If not set, default `user` will be used. + """ + + uid: Required[int] + """User ID (UID) for the Linux user. Must be a non-negative integer.""" + + username: Required[str] + """Username for the Linux user.""" + + +class LaunchParameters(TypedDict, total=False): + """ + LaunchParameters enable you to customize the resources available to your Devbox as well as the environment set up that should be completed before the Devbox is marked as 'running'. + """ + + after_idle: Optional[AfterIdle] + """Configure Devbox lifecycle based on idle activity. + + If after_idle is set, Devbox will ignore keep_alive_time_seconds. + """ + + architecture: Optional[Literal["x86_64", "arm64"]] + """The target architecture for the Devbox. If unset, defaults to x86_64.""" + + available_ports: Optional[Iterable[int]] + """A list of ports to make available on the Devbox. + + Only ports made available will be surfaced to create tunnels via the + 'createTunnel' API. + """ + + custom_cpu_cores: Optional[int] + """Custom CPU cores. Must be 0.5, 1, or a multiple of 2. Max is 16.""" + + custom_disk_size: Optional[int] + """Custom disk size in GiB. Must be a multiple of 2. Min is 2GiB, max is 64GiB.""" + + custom_gb_memory: Optional[int] + """Custom memory size in GiB. Must be 1 or a multiple of 2. Max is 64GiB.""" + + keep_alive_time_seconds: Optional[int] + """Time in seconds after which Devbox will automatically shutdown. + + Default is 1 hour. Maximum is 48 hours (172800 seconds). + """ + + launch_commands: Optional[SequenceNotStr[str]] + """Set of commands to be run at launch time, before the entrypoint process is run.""" + + network_policy_id: Optional[str] + """ + (Optional) ID of the network policy to apply to Devboxes launched with these + parameters. When set on a Blueprint launch parameters, Devboxes created from it + will inherit this policy unless explicitly overridden. + """ + + required_services: Optional[SequenceNotStr[str]] + """A list of ContainerizedService names to be started when a Devbox is created. + + A valid ContainerizedService must be specified in Blueprint to be started. + """ + + resource_size_request: Optional[ + Literal["X_SMALL", "SMALL", "MEDIUM", "LARGE", "X_LARGE", "XX_LARGE", "CUSTOM_SIZE"] + ] + """Manual resource configuration for Devbox. If not set, defaults will be used.""" + + user_parameters: Optional[UserParameters] + """Specify the user for execution on Devbox. + + If not set, default `user` will be used. + """ diff --git a/src/runloop_api_client/types/shared_params/mount.py b/src/runloop_api_client/types/shared_params/mount.py new file mode 100644 index 000000000..8c053506c --- /dev/null +++ b/src/runloop_api_client/types/shared_params/mount.py @@ -0,0 +1,43 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union, Optional +from typing_extensions import Literal, Required, TypeAlias, TypedDict + +from .agent_mount import AgentMount +from .object_mount import ObjectMount + +__all__ = ["Mount", "CodeMount", "FileMount"] + + +class CodeMount(TypedDict, total=False): + repo_name: Required[str] + """The name of the repo to mount. + + By default, code will be mounted at /home/user/{repo_name}s. + """ + + repo_owner: Required[str] + """The owner of the repo.""" + + type: Required[Literal["code_mount"]] + + token: Optional[str] + """The authentication token necessary to pull repo.""" + + install_command: Optional[str] + """Installation command to install and setup repository.""" + + +class FileMount(TypedDict, total=False): + content: Required[str] + """Content of the file to mount.""" + + target: Required[str] + """Target path where the file should be mounted.""" + + type: Required[Literal["file_mount"]] + + +Mount: TypeAlias = Union[ObjectMount, AgentMount, CodeMount, FileMount] diff --git a/src/runloop_api_client/types/shared_params/object_mount.py b/src/runloop_api_client/types/shared_params/object_mount.py new file mode 100644 index 000000000..1469897bf --- /dev/null +++ b/src/runloop_api_client/types/shared_params/object_mount.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 Literal, Required, TypedDict + +__all__ = ["ObjectMount"] + + +class ObjectMount(TypedDict, total=False): + object_id: Required[str] + """The ID of the object to write.""" + + object_path: Required[str] + """The path to write the object on the Devbox. + + Use absolute path of object (ie /home/user/object.txt, or directory if archive + /home/user/archive_dir) + """ + + type: Required[Literal["object_mount"]] diff --git a/src/runloop_api_client/types/shared_params/run_profile.py b/src/runloop_api_client/types/shared_params/run_profile.py new file mode 100644 index 000000000..10f82d5f7 --- /dev/null +++ b/src/runloop_api_client/types/shared_params/run_profile.py @@ -0,0 +1,38 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, Iterable, Optional +from typing_extensions import Annotated, TypedDict + +from .mount import Mount +from ..._utils import PropertyInfo +from .launch_parameters import LaunchParameters + +__all__ = ["RunProfile"] + + +class RunProfile(TypedDict, total=False): + env_vars: Annotated[Optional[Dict[str, str]], PropertyInfo(alias="envVars")] + """Mapping of Environment Variable to Value. + + May be shown in devbox logging. Example: {"DB_PASS": "DATABASE_PASSWORD"} would + set the environment variable 'DB_PASS' to the value 'DATABASE_PASSWORD_VALUE'. + """ + + launch_parameters: Annotated[Optional[LaunchParameters], PropertyInfo(alias="launchParameters")] + """Additional runtime LaunchParameters to apply after the devbox starts.""" + + mounts: Optional[Iterable[Mount]] + """A list of mounts to be included in the scenario run.""" + + purpose: Optional[str] + """Purpose of the run.""" + + secrets: Optional[Dict[str, str]] + """Mapping of Environment Variable to User Secret Name. + + Never shown in devbox logging. Example: {"DB_PASS": "DATABASE_PASSWORD"} would + set the environment variable 'DB_PASS' to the value of the secret + 'DATABASE_PASSWORD'. + """ diff --git a/src/runloop_api_client/types/tunnel_view.py b/src/runloop_api_client/types/tunnel_view.py new file mode 100644 index 000000000..9287cd4d2 --- /dev/null +++ b/src/runloop_api_client/types/tunnel_view.py @@ -0,0 +1,40 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["TunnelView"] + + +class TunnelView(BaseModel): + """A V2 tunnel provides secure HTTP access to services running on a Devbox. + + Tunnels allow external clients to reach web servers, APIs, or other HTTP services running inside a Devbox without requiring direct network access. Each tunnel is uniquely identified by an encrypted tunnel_key and can be configured for either open (public) or authenticated access. + Usage: https://{port}-{tunnel_key}.tunnel.runloop.ai + """ + + auth_mode: Literal["open", "authenticated"] + """The authentication mode for the tunnel.""" + + create_time_ms: int + """Creation time of the tunnel (Unix timestamp milliseconds).""" + + http_keep_alive: bool + """ + When true, HTTP traffic through the tunnel counts as activity for idle lifecycle + policies, resetting the idle timer. + """ + + tunnel_key: str + """The encrypted tunnel key used to construct the tunnel URL. + + URL format: https://{port}-{tunnel_key}.tunnel.runloop.{domain} + """ + + auth_token: Optional[str] = None + """Bearer token for tunnel authentication. + + Only present when auth_mode is 'authenticated'. + """ diff --git a/src/runloop_minus_api_minus_client/lib/.keep b/src/runloop_minus_api_minus_client/lib/.keep new file mode 100644 index 000000000..5e2c99fdb --- /dev/null +++ b/src/runloop_minus_api_minus_client/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/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..fd8019a9a --- /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 000000000..fd8019a9a --- /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/devboxes/__init__.py b/tests/api_resources/devboxes/__init__.py new file mode 100644 index 000000000..fd8019a9a --- /dev/null +++ b/tests/api_resources/devboxes/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/devboxes/test_browsers.py b/tests/api_resources/devboxes/test_browsers.py new file mode 100644 index 000000000..1e72e079e --- /dev/null +++ b/tests/api_resources/devboxes/test_browsers.py @@ -0,0 +1,164 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from runloop_api_client import Runloop, AsyncRunloop +from runloop_api_client.types.devboxes import BrowserView + +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"]) + + @parametrize + def test_method_create(self, client: Runloop) -> None: + browser = client.devboxes.browsers.create() + assert_matches_type(BrowserView, browser, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: Runloop) -> None: + browser = client.devboxes.browsers.create( + name="name", + ) + assert_matches_type(BrowserView, browser, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Runloop) -> None: + response = client.devboxes.browsers.with_raw_response.create() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser = response.parse() + assert_matches_type(BrowserView, browser, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Runloop) -> None: + with client.devboxes.browsers.with_streaming_response.create() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser = response.parse() + assert_matches_type(BrowserView, browser, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_retrieve(self, client: Runloop) -> None: + browser = client.devboxes.browsers.retrieve( + "id", + ) + assert_matches_type(BrowserView, browser, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Runloop) -> None: + response = client.devboxes.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(BrowserView, browser, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Runloop) -> None: + with client.devboxes.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(BrowserView, browser, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.browsers.with_raw_response.retrieve( + "", + ) + + +class TestAsyncBrowsers: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_create(self, async_client: AsyncRunloop) -> None: + browser = await async_client.devboxes.browsers.create() + assert_matches_type(BrowserView, browser, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncRunloop) -> None: + browser = await async_client.devboxes.browsers.create( + name="name", + ) + assert_matches_type(BrowserView, browser, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.browsers.with_raw_response.create() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + browser = await response.parse() + assert_matches_type(BrowserView, browser, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.browsers.with_streaming_response.create() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + browser = await response.parse() + assert_matches_type(BrowserView, browser, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_retrieve(self, async_client: AsyncRunloop) -> None: + browser = await async_client.devboxes.browsers.retrieve( + "id", + ) + assert_matches_type(BrowserView, browser, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.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(BrowserView, browser, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.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(BrowserView, browser, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.browsers.with_raw_response.retrieve( + "", + ) diff --git a/tests/api_resources/devboxes/test_computers.py b/tests/api_resources/devboxes/test_computers.py new file mode 100644 index 000000000..a7cbf4e52 --- /dev/null +++ b/tests/api_resources/devboxes/test_computers.py @@ -0,0 +1,471 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from runloop_api_client import Runloop, AsyncRunloop +from runloop_api_client.types.devboxes import ( + ComputerView, + ComputerMouseInteractionResponse, + ComputerScreenInteractionResponse, + ComputerKeyboardInteractionResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestComputers: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Runloop) -> None: + computer = client.devboxes.computers.create() + assert_matches_type(ComputerView, computer, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: Runloop) -> None: + computer = client.devboxes.computers.create( + display_dimensions={ + "display_height_px": 0, + "display_width_px": 0, + }, + name="name", + ) + assert_matches_type(ComputerView, computer, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Runloop) -> None: + response = client.devboxes.computers.with_raw_response.create() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + computer = response.parse() + assert_matches_type(ComputerView, computer, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Runloop) -> None: + with client.devboxes.computers.with_streaming_response.create() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + computer = response.parse() + assert_matches_type(ComputerView, computer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_retrieve(self, client: Runloop) -> None: + computer = client.devboxes.computers.retrieve( + "id", + ) + assert_matches_type(ComputerView, computer, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Runloop) -> None: + response = client.devboxes.computers.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + computer = response.parse() + assert_matches_type(ComputerView, computer, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Runloop) -> None: + with client.devboxes.computers.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + computer = response.parse() + assert_matches_type(ComputerView, computer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.computers.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_keyboard_interaction(self, client: Runloop) -> None: + computer = client.devboxes.computers.keyboard_interaction( + id="id", + action="key", + ) + assert_matches_type(ComputerKeyboardInteractionResponse, computer, path=["response"]) + + @parametrize + def test_method_keyboard_interaction_with_all_params(self, client: Runloop) -> None: + computer = client.devboxes.computers.keyboard_interaction( + id="id", + action="key", + text="text", + ) + assert_matches_type(ComputerKeyboardInteractionResponse, computer, path=["response"]) + + @parametrize + def test_raw_response_keyboard_interaction(self, client: Runloop) -> None: + response = client.devboxes.computers.with_raw_response.keyboard_interaction( + id="id", + action="key", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + computer = response.parse() + assert_matches_type(ComputerKeyboardInteractionResponse, computer, path=["response"]) + + @parametrize + def test_streaming_response_keyboard_interaction(self, client: Runloop) -> None: + with client.devboxes.computers.with_streaming_response.keyboard_interaction( + id="id", + action="key", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + computer = response.parse() + assert_matches_type(ComputerKeyboardInteractionResponse, computer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_keyboard_interaction(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.computers.with_raw_response.keyboard_interaction( + id="", + action="key", + ) + + @parametrize + def test_method_mouse_interaction(self, client: Runloop) -> None: + computer = client.devboxes.computers.mouse_interaction( + id="id", + action="mouse_move", + ) + assert_matches_type(ComputerMouseInteractionResponse, computer, path=["response"]) + + @parametrize + def test_method_mouse_interaction_with_all_params(self, client: Runloop) -> None: + computer = client.devboxes.computers.mouse_interaction( + id="id", + action="mouse_move", + coordinate={ + "x": 0, + "y": 0, + }, + ) + assert_matches_type(ComputerMouseInteractionResponse, computer, path=["response"]) + + @parametrize + def test_raw_response_mouse_interaction(self, client: Runloop) -> None: + response = client.devboxes.computers.with_raw_response.mouse_interaction( + id="id", + action="mouse_move", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + computer = response.parse() + assert_matches_type(ComputerMouseInteractionResponse, computer, path=["response"]) + + @parametrize + def test_streaming_response_mouse_interaction(self, client: Runloop) -> None: + with client.devboxes.computers.with_streaming_response.mouse_interaction( + id="id", + action="mouse_move", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + computer = response.parse() + assert_matches_type(ComputerMouseInteractionResponse, computer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_mouse_interaction(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.computers.with_raw_response.mouse_interaction( + id="", + action="mouse_move", + ) + + @parametrize + def test_method_screen_interaction(self, client: Runloop) -> None: + computer = client.devboxes.computers.screen_interaction( + id="id", + action="screenshot", + ) + assert_matches_type(ComputerScreenInteractionResponse, computer, path=["response"]) + + @parametrize + def test_raw_response_screen_interaction(self, client: Runloop) -> None: + response = client.devboxes.computers.with_raw_response.screen_interaction( + id="id", + action="screenshot", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + computer = response.parse() + assert_matches_type(ComputerScreenInteractionResponse, computer, path=["response"]) + + @parametrize + def test_streaming_response_screen_interaction(self, client: Runloop) -> None: + with client.devboxes.computers.with_streaming_response.screen_interaction( + id="id", + action="screenshot", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + computer = response.parse() + assert_matches_type(ComputerScreenInteractionResponse, computer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_screen_interaction(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.computers.with_raw_response.screen_interaction( + id="", + action="screenshot", + ) + + +class TestAsyncComputers: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_create(self, async_client: AsyncRunloop) -> None: + computer = await async_client.devboxes.computers.create() + assert_matches_type(ComputerView, computer, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncRunloop) -> None: + computer = await async_client.devboxes.computers.create( + display_dimensions={ + "display_height_px": 0, + "display_width_px": 0, + }, + name="name", + ) + assert_matches_type(ComputerView, computer, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.computers.with_raw_response.create() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + computer = await response.parse() + assert_matches_type(ComputerView, computer, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.computers.with_streaming_response.create() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + computer = await response.parse() + assert_matches_type(ComputerView, computer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_retrieve(self, async_client: AsyncRunloop) -> None: + computer = await async_client.devboxes.computers.retrieve( + "id", + ) + assert_matches_type(ComputerView, computer, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.computers.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + computer = await response.parse() + assert_matches_type(ComputerView, computer, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.computers.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + computer = await response.parse() + assert_matches_type(ComputerView, computer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.computers.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_keyboard_interaction(self, async_client: AsyncRunloop) -> None: + computer = await async_client.devboxes.computers.keyboard_interaction( + id="id", + action="key", + ) + assert_matches_type(ComputerKeyboardInteractionResponse, computer, path=["response"]) + + @parametrize + async def test_method_keyboard_interaction_with_all_params(self, async_client: AsyncRunloop) -> None: + computer = await async_client.devboxes.computers.keyboard_interaction( + id="id", + action="key", + text="text", + ) + assert_matches_type(ComputerKeyboardInteractionResponse, computer, path=["response"]) + + @parametrize + async def test_raw_response_keyboard_interaction(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.computers.with_raw_response.keyboard_interaction( + id="id", + action="key", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + computer = await response.parse() + assert_matches_type(ComputerKeyboardInteractionResponse, computer, path=["response"]) + + @parametrize + async def test_streaming_response_keyboard_interaction(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.computers.with_streaming_response.keyboard_interaction( + id="id", + action="key", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + computer = await response.parse() + assert_matches_type(ComputerKeyboardInteractionResponse, computer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_keyboard_interaction(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.computers.with_raw_response.keyboard_interaction( + id="", + action="key", + ) + + @parametrize + async def test_method_mouse_interaction(self, async_client: AsyncRunloop) -> None: + computer = await async_client.devboxes.computers.mouse_interaction( + id="id", + action="mouse_move", + ) + assert_matches_type(ComputerMouseInteractionResponse, computer, path=["response"]) + + @parametrize + async def test_method_mouse_interaction_with_all_params(self, async_client: AsyncRunloop) -> None: + computer = await async_client.devboxes.computers.mouse_interaction( + id="id", + action="mouse_move", + coordinate={ + "x": 0, + "y": 0, + }, + ) + assert_matches_type(ComputerMouseInteractionResponse, computer, path=["response"]) + + @parametrize + async def test_raw_response_mouse_interaction(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.computers.with_raw_response.mouse_interaction( + id="id", + action="mouse_move", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + computer = await response.parse() + assert_matches_type(ComputerMouseInteractionResponse, computer, path=["response"]) + + @parametrize + async def test_streaming_response_mouse_interaction(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.computers.with_streaming_response.mouse_interaction( + id="id", + action="mouse_move", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + computer = await response.parse() + assert_matches_type(ComputerMouseInteractionResponse, computer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_mouse_interaction(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.computers.with_raw_response.mouse_interaction( + id="", + action="mouse_move", + ) + + @parametrize + async def test_method_screen_interaction(self, async_client: AsyncRunloop) -> None: + computer = await async_client.devboxes.computers.screen_interaction( + id="id", + action="screenshot", + ) + assert_matches_type(ComputerScreenInteractionResponse, computer, path=["response"]) + + @parametrize + async def test_raw_response_screen_interaction(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.computers.with_raw_response.screen_interaction( + id="id", + action="screenshot", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + computer = await response.parse() + assert_matches_type(ComputerScreenInteractionResponse, computer, path=["response"]) + + @parametrize + async def test_streaming_response_screen_interaction(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.computers.with_streaming_response.screen_interaction( + id="id", + action="screenshot", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + computer = await response.parse() + assert_matches_type(ComputerScreenInteractionResponse, computer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_screen_interaction(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.computers.with_raw_response.screen_interaction( + id="", + action="screenshot", + ) diff --git a/tests/api_resources/devboxes/test_disk_snapshots.py b/tests/api_resources/devboxes/test_disk_snapshots.py new file mode 100644 index 000000000..88e3310a9 --- /dev/null +++ b/tests/api_resources/devboxes/test_disk_snapshots.py @@ -0,0 +1,350 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from runloop_api_client import Runloop, AsyncRunloop +from runloop_api_client.types import DevboxSnapshotView +from runloop_api_client.pagination import SyncDiskSnapshotsCursorIDPage, AsyncDiskSnapshotsCursorIDPage +from runloop_api_client.types.devboxes import ( + DevboxSnapshotAsyncStatusView, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestDiskSnapshots: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_update(self, client: Runloop) -> None: + disk_snapshot = client.devboxes.disk_snapshots.update( + id="id", + ) + assert_matches_type(DevboxSnapshotView, disk_snapshot, path=["response"]) + + @parametrize + def test_method_update_with_all_params(self, client: Runloop) -> None: + disk_snapshot = client.devboxes.disk_snapshots.update( + id="id", + commit_message="commit_message", + metadata={"foo": "string"}, + name="name", + ) + assert_matches_type(DevboxSnapshotView, disk_snapshot, path=["response"]) + + @parametrize + def test_raw_response_update(self, client: Runloop) -> None: + response = client.devboxes.disk_snapshots.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + disk_snapshot = response.parse() + assert_matches_type(DevboxSnapshotView, disk_snapshot, path=["response"]) + + @parametrize + def test_streaming_response_update(self, client: Runloop) -> None: + with client.devboxes.disk_snapshots.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + disk_snapshot = response.parse() + assert_matches_type(DevboxSnapshotView, disk_snapshot, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_update(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.disk_snapshots.with_raw_response.update( + id="", + ) + + @parametrize + def test_method_list(self, client: Runloop) -> None: + disk_snapshot = client.devboxes.disk_snapshots.list() + assert_matches_type(SyncDiskSnapshotsCursorIDPage[DevboxSnapshotView], disk_snapshot, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Runloop) -> None: + disk_snapshot = client.devboxes.disk_snapshots.list( + devbox_id="devbox_id", + limit=0, + metadata_key="metadata[key]", + metadata_key_in="metadata[key][in]", + source_blueprint_id="source_blueprint_id", + starting_after="starting_after", + ) + assert_matches_type(SyncDiskSnapshotsCursorIDPage[DevboxSnapshotView], disk_snapshot, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Runloop) -> None: + response = client.devboxes.disk_snapshots.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + disk_snapshot = response.parse() + assert_matches_type(SyncDiskSnapshotsCursorIDPage[DevboxSnapshotView], disk_snapshot, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Runloop) -> None: + with client.devboxes.disk_snapshots.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + disk_snapshot = response.parse() + assert_matches_type(SyncDiskSnapshotsCursorIDPage[DevboxSnapshotView], disk_snapshot, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_delete(self, client: Runloop) -> None: + disk_snapshot = client.devboxes.disk_snapshots.delete( + "id", + ) + assert_matches_type(object, disk_snapshot, path=["response"]) + + @parametrize + def test_raw_response_delete(self, client: Runloop) -> None: + response = client.devboxes.disk_snapshots.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + disk_snapshot = response.parse() + assert_matches_type(object, disk_snapshot, path=["response"]) + + @parametrize + def test_streaming_response_delete(self, client: Runloop) -> None: + with client.devboxes.disk_snapshots.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + disk_snapshot = response.parse() + assert_matches_type(object, disk_snapshot, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_delete(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.disk_snapshots.with_raw_response.delete( + "", + ) + + @parametrize + def test_method_query_status(self, client: Runloop) -> None: + disk_snapshot = client.devboxes.disk_snapshots.query_status( + "id", + ) + assert_matches_type(DevboxSnapshotAsyncStatusView, disk_snapshot, path=["response"]) + + @parametrize + def test_raw_response_query_status(self, client: Runloop) -> None: + response = client.devboxes.disk_snapshots.with_raw_response.query_status( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + disk_snapshot = response.parse() + assert_matches_type(DevboxSnapshotAsyncStatusView, disk_snapshot, path=["response"]) + + @parametrize + def test_streaming_response_query_status(self, client: Runloop) -> None: + with client.devboxes.disk_snapshots.with_streaming_response.query_status( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + disk_snapshot = response.parse() + assert_matches_type(DevboxSnapshotAsyncStatusView, disk_snapshot, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_query_status(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.disk_snapshots.with_raw_response.query_status( + "", + ) + + +class TestAsyncDiskSnapshots: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_update(self, async_client: AsyncRunloop) -> None: + disk_snapshot = await async_client.devboxes.disk_snapshots.update( + id="id", + ) + assert_matches_type(DevboxSnapshotView, disk_snapshot, path=["response"]) + + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncRunloop) -> None: + disk_snapshot = await async_client.devboxes.disk_snapshots.update( + id="id", + commit_message="commit_message", + metadata={"foo": "string"}, + name="name", + ) + assert_matches_type(DevboxSnapshotView, disk_snapshot, path=["response"]) + + @parametrize + async def test_raw_response_update(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.disk_snapshots.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + disk_snapshot = await response.parse() + assert_matches_type(DevboxSnapshotView, disk_snapshot, path=["response"]) + + @parametrize + async def test_streaming_response_update(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.disk_snapshots.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + disk_snapshot = await response.parse() + assert_matches_type(DevboxSnapshotView, disk_snapshot, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_update(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.disk_snapshots.with_raw_response.update( + id="", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncRunloop) -> None: + disk_snapshot = await async_client.devboxes.disk_snapshots.list() + assert_matches_type(AsyncDiskSnapshotsCursorIDPage[DevboxSnapshotView], disk_snapshot, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncRunloop) -> None: + disk_snapshot = await async_client.devboxes.disk_snapshots.list( + devbox_id="devbox_id", + limit=0, + metadata_key="metadata[key]", + metadata_key_in="metadata[key][in]", + source_blueprint_id="source_blueprint_id", + starting_after="starting_after", + ) + assert_matches_type(AsyncDiskSnapshotsCursorIDPage[DevboxSnapshotView], disk_snapshot, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.disk_snapshots.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + disk_snapshot = await response.parse() + assert_matches_type(AsyncDiskSnapshotsCursorIDPage[DevboxSnapshotView], disk_snapshot, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.disk_snapshots.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + disk_snapshot = await response.parse() + assert_matches_type(AsyncDiskSnapshotsCursorIDPage[DevboxSnapshotView], disk_snapshot, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_delete(self, async_client: AsyncRunloop) -> None: + disk_snapshot = await async_client.devboxes.disk_snapshots.delete( + "id", + ) + assert_matches_type(object, disk_snapshot, path=["response"]) + + @parametrize + async def test_raw_response_delete(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.disk_snapshots.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + disk_snapshot = await response.parse() + assert_matches_type(object, disk_snapshot, path=["response"]) + + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.disk_snapshots.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + disk_snapshot = await response.parse() + assert_matches_type(object, disk_snapshot, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_delete(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.disk_snapshots.with_raw_response.delete( + "", + ) + + @parametrize + async def test_method_query_status(self, async_client: AsyncRunloop) -> None: + disk_snapshot = await async_client.devboxes.disk_snapshots.query_status( + "id", + ) + assert_matches_type(DevboxSnapshotAsyncStatusView, disk_snapshot, path=["response"]) + + @parametrize + async def test_raw_response_query_status(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.disk_snapshots.with_raw_response.query_status( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + disk_snapshot = await response.parse() + assert_matches_type(DevboxSnapshotAsyncStatusView, disk_snapshot, path=["response"]) + + @parametrize + async def test_streaming_response_query_status(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.disk_snapshots.with_streaming_response.query_status( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + disk_snapshot = await response.parse() + assert_matches_type(DevboxSnapshotAsyncStatusView, disk_snapshot, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_query_status(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.disk_snapshots.with_raw_response.query_status( + "", + ) diff --git a/tests/api_resources/devboxes/test_executions.py b/tests/api_resources/devboxes/test_executions.py new file mode 100755 index 000000000..03f025f09 --- /dev/null +++ b/tests/api_resources/devboxes/test_executions.py @@ -0,0 +1,816 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from runloop_api_client import Runloop, AsyncRunloop +from runloop_api_client.types import DevboxSendStdInResult, DevboxExecutionDetailView, DevboxAsyncExecutionDetailView + +# pyright: reportDeprecated=false + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestExecutions: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_retrieve(self, client: Runloop) -> None: + execution = client.devboxes.executions.retrieve( + execution_id="execution_id", + devbox_id="devbox_id", + ) + assert_matches_type(DevboxAsyncExecutionDetailView, execution, path=["response"]) + + @parametrize + def test_method_retrieve_with_all_params(self, client: Runloop) -> None: + execution = client.devboxes.executions.retrieve( + execution_id="execution_id", + devbox_id="devbox_id", + last_n="last_n", + ) + assert_matches_type(DevboxAsyncExecutionDetailView, execution, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Runloop) -> None: + response = client.devboxes.executions.with_raw_response.retrieve( + execution_id="execution_id", + devbox_id="devbox_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + execution = response.parse() + assert_matches_type(DevboxAsyncExecutionDetailView, execution, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Runloop) -> None: + with client.devboxes.executions.with_streaming_response.retrieve( + execution_id="execution_id", + devbox_id="devbox_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + execution = response.parse() + assert_matches_type(DevboxAsyncExecutionDetailView, execution, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `devbox_id` but received ''"): + client.devboxes.executions.with_raw_response.retrieve( + execution_id="execution_id", + devbox_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `execution_id` but received ''"): + client.devboxes.executions.with_raw_response.retrieve( + execution_id="", + devbox_id="devbox_id", + ) + + @parametrize + def test_method_execute_async(self, client: Runloop) -> None: + execution = client.devboxes.executions.execute_async( + id="id", + command="command", + ) + assert_matches_type(DevboxAsyncExecutionDetailView, execution, path=["response"]) + + @parametrize + def test_method_execute_async_with_all_params(self, client: Runloop) -> None: + execution = client.devboxes.executions.execute_async( + id="id", + command="command", + attach_stdin=True, + shell_name="shell_name", + ) + assert_matches_type(DevboxAsyncExecutionDetailView, execution, path=["response"]) + + @parametrize + def test_raw_response_execute_async(self, client: Runloop) -> None: + response = client.devboxes.executions.with_raw_response.execute_async( + id="id", + command="command", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + execution = response.parse() + assert_matches_type(DevboxAsyncExecutionDetailView, execution, path=["response"]) + + @parametrize + def test_streaming_response_execute_async(self, client: Runloop) -> None: + with client.devboxes.executions.with_streaming_response.execute_async( + id="id", + command="command", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + execution = response.parse() + assert_matches_type(DevboxAsyncExecutionDetailView, execution, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_execute_async(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.executions.with_raw_response.execute_async( + id="", + command="command", + ) + + @parametrize + def test_method_execute_sync(self, client: Runloop) -> None: + with pytest.warns(DeprecationWarning): + execution = client.devboxes.executions.execute_sync( + id="id", + command="command", + ) + + assert_matches_type(DevboxExecutionDetailView, execution, path=["response"]) + + @parametrize + def test_method_execute_sync_with_all_params(self, client: Runloop) -> None: + with pytest.warns(DeprecationWarning): + execution = client.devboxes.executions.execute_sync( + id="id", + command="command", + attach_stdin=True, + shell_name="shell_name", + ) + + assert_matches_type(DevboxExecutionDetailView, execution, path=["response"]) + + @parametrize + def test_raw_response_execute_sync(self, client: Runloop) -> None: + with pytest.warns(DeprecationWarning): + response = client.devboxes.executions.with_raw_response.execute_sync( + id="id", + command="command", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + execution = response.parse() + assert_matches_type(DevboxExecutionDetailView, execution, path=["response"]) + + @parametrize + def test_streaming_response_execute_sync(self, client: Runloop) -> None: + with pytest.warns(DeprecationWarning): + with client.devboxes.executions.with_streaming_response.execute_sync( + id="id", + command="command", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + execution = response.parse() + assert_matches_type(DevboxExecutionDetailView, execution, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_execute_sync(self, client: Runloop) -> None: + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.executions.with_raw_response.execute_sync( + id="", + command="command", + ) + + @parametrize + def test_method_kill(self, client: Runloop) -> None: + execution = client.devboxes.executions.kill( + execution_id="execution_id", + devbox_id="devbox_id", + ) + assert_matches_type(DevboxAsyncExecutionDetailView, execution, path=["response"]) + + @parametrize + def test_method_kill_with_all_params(self, client: Runloop) -> None: + execution = client.devboxes.executions.kill( + execution_id="execution_id", + devbox_id="devbox_id", + kill_process_group=True, + ) + assert_matches_type(DevboxAsyncExecutionDetailView, execution, path=["response"]) + + @parametrize + def test_raw_response_kill(self, client: Runloop) -> None: + response = client.devboxes.executions.with_raw_response.kill( + execution_id="execution_id", + devbox_id="devbox_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + execution = response.parse() + assert_matches_type(DevboxAsyncExecutionDetailView, execution, path=["response"]) + + @parametrize + def test_streaming_response_kill(self, client: Runloop) -> None: + with client.devboxes.executions.with_streaming_response.kill( + execution_id="execution_id", + devbox_id="devbox_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + execution = response.parse() + assert_matches_type(DevboxAsyncExecutionDetailView, execution, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_kill(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `devbox_id` but received ''"): + client.devboxes.executions.with_raw_response.kill( + execution_id="execution_id", + devbox_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `execution_id` but received ''"): + client.devboxes.executions.with_raw_response.kill( + execution_id="", + devbox_id="devbox_id", + ) + + @parametrize + def test_method_send_std_in(self, client: Runloop) -> None: + execution = client.devboxes.executions.send_std_in( + execution_id="execution_id", + devbox_id="devbox_id", + ) + assert_matches_type(DevboxSendStdInResult, execution, path=["response"]) + + @parametrize + def test_method_send_std_in_with_all_params(self, client: Runloop) -> None: + execution = client.devboxes.executions.send_std_in( + execution_id="execution_id", + devbox_id="devbox_id", + signal="EOF", + text="text", + ) + assert_matches_type(DevboxSendStdInResult, execution, path=["response"]) + + @parametrize + def test_raw_response_send_std_in(self, client: Runloop) -> None: + response = client.devboxes.executions.with_raw_response.send_std_in( + execution_id="execution_id", + devbox_id="devbox_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + execution = response.parse() + assert_matches_type(DevboxSendStdInResult, execution, path=["response"]) + + @parametrize + def test_streaming_response_send_std_in(self, client: Runloop) -> None: + with client.devboxes.executions.with_streaming_response.send_std_in( + execution_id="execution_id", + devbox_id="devbox_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + execution = response.parse() + assert_matches_type(DevboxSendStdInResult, execution, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_send_std_in(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `devbox_id` but received ''"): + client.devboxes.executions.with_raw_response.send_std_in( + execution_id="execution_id", + devbox_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `execution_id` but received ''"): + client.devboxes.executions.with_raw_response.send_std_in( + execution_id="", + devbox_id="devbox_id", + ) + + @parametrize + def test_method_stream_stderr_updates(self, client: Runloop) -> None: + execution_stream = client.devboxes.executions.stream_stderr_updates( + execution_id="execution_id", + devbox_id="devbox_id", + ) + execution_stream.response.close() + + @parametrize + def test_method_stream_stderr_updates_with_all_params(self, client: Runloop) -> None: + execution_stream = client.devboxes.executions.stream_stderr_updates( + execution_id="execution_id", + devbox_id="devbox_id", + offset="offset", + ) + execution_stream.response.close() + + @parametrize + def test_raw_response_stream_stderr_updates(self, client: Runloop) -> None: + response = client.devboxes.executions.with_raw_response.stream_stderr_updates( + execution_id="execution_id", + devbox_id="devbox_id", + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = response.parse() + stream.close() + + @parametrize + def test_streaming_response_stream_stderr_updates(self, client: Runloop) -> None: + with client.devboxes.executions.with_streaming_response.stream_stderr_updates( + execution_id="execution_id", + devbox_id="devbox_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 + + @parametrize + def test_path_params_stream_stderr_updates(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `devbox_id` but received ''"): + client.devboxes.executions.with_raw_response.stream_stderr_updates( + execution_id="execution_id", + devbox_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `execution_id` but received ''"): + client.devboxes.executions.with_raw_response.stream_stderr_updates( + execution_id="", + devbox_id="devbox_id", + ) + + @parametrize + def test_method_stream_stdout_updates(self, client: Runloop) -> None: + execution_stream = client.devboxes.executions.stream_stdout_updates( + execution_id="execution_id", + devbox_id="devbox_id", + ) + execution_stream.response.close() + + @parametrize + def test_method_stream_stdout_updates_with_all_params(self, client: Runloop) -> None: + execution_stream = client.devboxes.executions.stream_stdout_updates( + execution_id="execution_id", + devbox_id="devbox_id", + offset="offset", + ) + execution_stream.response.close() + + @parametrize + def test_raw_response_stream_stdout_updates(self, client: Runloop) -> None: + response = client.devboxes.executions.with_raw_response.stream_stdout_updates( + execution_id="execution_id", + devbox_id="devbox_id", + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = response.parse() + stream.close() + + @parametrize + def test_streaming_response_stream_stdout_updates(self, client: Runloop) -> None: + with client.devboxes.executions.with_streaming_response.stream_stdout_updates( + execution_id="execution_id", + devbox_id="devbox_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 + + @parametrize + def test_path_params_stream_stdout_updates(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `devbox_id` but received ''"): + client.devboxes.executions.with_raw_response.stream_stdout_updates( + execution_id="execution_id", + devbox_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `execution_id` but received ''"): + client.devboxes.executions.with_raw_response.stream_stdout_updates( + execution_id="", + devbox_id="devbox_id", + ) + + +class TestAsyncExecutions: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_retrieve(self, async_client: AsyncRunloop) -> None: + execution = await async_client.devboxes.executions.retrieve( + execution_id="execution_id", + devbox_id="devbox_id", + ) + assert_matches_type(DevboxAsyncExecutionDetailView, execution, path=["response"]) + + @parametrize + async def test_method_retrieve_with_all_params(self, async_client: AsyncRunloop) -> None: + execution = await async_client.devboxes.executions.retrieve( + execution_id="execution_id", + devbox_id="devbox_id", + last_n="last_n", + ) + assert_matches_type(DevboxAsyncExecutionDetailView, execution, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.executions.with_raw_response.retrieve( + execution_id="execution_id", + devbox_id="devbox_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + execution = await response.parse() + assert_matches_type(DevboxAsyncExecutionDetailView, execution, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.executions.with_streaming_response.retrieve( + execution_id="execution_id", + devbox_id="devbox_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + execution = await response.parse() + assert_matches_type(DevboxAsyncExecutionDetailView, execution, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `devbox_id` but received ''"): + await async_client.devboxes.executions.with_raw_response.retrieve( + execution_id="execution_id", + devbox_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `execution_id` but received ''"): + await async_client.devboxes.executions.with_raw_response.retrieve( + execution_id="", + devbox_id="devbox_id", + ) + + @parametrize + async def test_method_execute_async(self, async_client: AsyncRunloop) -> None: + execution = await async_client.devboxes.executions.execute_async( + id="id", + command="command", + ) + assert_matches_type(DevboxAsyncExecutionDetailView, execution, path=["response"]) + + @parametrize + async def test_method_execute_async_with_all_params(self, async_client: AsyncRunloop) -> None: + execution = await async_client.devboxes.executions.execute_async( + id="id", + command="command", + attach_stdin=True, + shell_name="shell_name", + ) + assert_matches_type(DevboxAsyncExecutionDetailView, execution, path=["response"]) + + @parametrize + async def test_raw_response_execute_async(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.executions.with_raw_response.execute_async( + id="id", + command="command", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + execution = await response.parse() + assert_matches_type(DevboxAsyncExecutionDetailView, execution, path=["response"]) + + @parametrize + async def test_streaming_response_execute_async(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.executions.with_streaming_response.execute_async( + id="id", + command="command", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + execution = await response.parse() + assert_matches_type(DevboxAsyncExecutionDetailView, execution, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_execute_async(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.executions.with_raw_response.execute_async( + id="", + command="command", + ) + + @parametrize + async def test_method_execute_sync(self, async_client: AsyncRunloop) -> None: + with pytest.warns(DeprecationWarning): + execution = await async_client.devboxes.executions.execute_sync( + id="id", + command="command", + ) + + assert_matches_type(DevboxExecutionDetailView, execution, path=["response"]) + + @parametrize + async def test_method_execute_sync_with_all_params(self, async_client: AsyncRunloop) -> None: + with pytest.warns(DeprecationWarning): + execution = await async_client.devboxes.executions.execute_sync( + id="id", + command="command", + attach_stdin=True, + shell_name="shell_name", + ) + + assert_matches_type(DevboxExecutionDetailView, execution, path=["response"]) + + @parametrize + async def test_raw_response_execute_sync(self, async_client: AsyncRunloop) -> None: + with pytest.warns(DeprecationWarning): + response = await async_client.devboxes.executions.with_raw_response.execute_sync( + id="id", + command="command", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + execution = await response.parse() + assert_matches_type(DevboxExecutionDetailView, execution, path=["response"]) + + @parametrize + async def test_streaming_response_execute_sync(self, async_client: AsyncRunloop) -> None: + with pytest.warns(DeprecationWarning): + async with async_client.devboxes.executions.with_streaming_response.execute_sync( + id="id", + command="command", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + execution = await response.parse() + assert_matches_type(DevboxExecutionDetailView, execution, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_execute_sync(self, async_client: AsyncRunloop) -> None: + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.executions.with_raw_response.execute_sync( + id="", + command="command", + ) + + @parametrize + async def test_method_kill(self, async_client: AsyncRunloop) -> None: + execution = await async_client.devboxes.executions.kill( + execution_id="execution_id", + devbox_id="devbox_id", + ) + assert_matches_type(DevboxAsyncExecutionDetailView, execution, path=["response"]) + + @parametrize + async def test_method_kill_with_all_params(self, async_client: AsyncRunloop) -> None: + execution = await async_client.devboxes.executions.kill( + execution_id="execution_id", + devbox_id="devbox_id", + kill_process_group=True, + ) + assert_matches_type(DevboxAsyncExecutionDetailView, execution, path=["response"]) + + @parametrize + async def test_raw_response_kill(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.executions.with_raw_response.kill( + execution_id="execution_id", + devbox_id="devbox_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + execution = await response.parse() + assert_matches_type(DevboxAsyncExecutionDetailView, execution, path=["response"]) + + @parametrize + async def test_streaming_response_kill(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.executions.with_streaming_response.kill( + execution_id="execution_id", + devbox_id="devbox_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + execution = await response.parse() + assert_matches_type(DevboxAsyncExecutionDetailView, execution, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_kill(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `devbox_id` but received ''"): + await async_client.devboxes.executions.with_raw_response.kill( + execution_id="execution_id", + devbox_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `execution_id` but received ''"): + await async_client.devboxes.executions.with_raw_response.kill( + execution_id="", + devbox_id="devbox_id", + ) + + @parametrize + async def test_method_send_std_in(self, async_client: AsyncRunloop) -> None: + execution = await async_client.devboxes.executions.send_std_in( + execution_id="execution_id", + devbox_id="devbox_id", + ) + assert_matches_type(DevboxSendStdInResult, execution, path=["response"]) + + @parametrize + async def test_method_send_std_in_with_all_params(self, async_client: AsyncRunloop) -> None: + execution = await async_client.devboxes.executions.send_std_in( + execution_id="execution_id", + devbox_id="devbox_id", + signal="EOF", + text="text", + ) + assert_matches_type(DevboxSendStdInResult, execution, path=["response"]) + + @parametrize + async def test_raw_response_send_std_in(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.executions.with_raw_response.send_std_in( + execution_id="execution_id", + devbox_id="devbox_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + execution = await response.parse() + assert_matches_type(DevboxSendStdInResult, execution, path=["response"]) + + @parametrize + async def test_streaming_response_send_std_in(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.executions.with_streaming_response.send_std_in( + execution_id="execution_id", + devbox_id="devbox_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + execution = await response.parse() + assert_matches_type(DevboxSendStdInResult, execution, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_send_std_in(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `devbox_id` but received ''"): + await async_client.devboxes.executions.with_raw_response.send_std_in( + execution_id="execution_id", + devbox_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `execution_id` but received ''"): + await async_client.devboxes.executions.with_raw_response.send_std_in( + execution_id="", + devbox_id="devbox_id", + ) + + @parametrize + async def test_method_stream_stderr_updates(self, async_client: AsyncRunloop) -> None: + execution_stream = await async_client.devboxes.executions.stream_stderr_updates( + execution_id="execution_id", + devbox_id="devbox_id", + ) + await execution_stream.response.aclose() + + @parametrize + async def test_method_stream_stderr_updates_with_all_params(self, async_client: AsyncRunloop) -> None: + execution_stream = await async_client.devboxes.executions.stream_stderr_updates( + execution_id="execution_id", + devbox_id="devbox_id", + offset="offset", + ) + await execution_stream.response.aclose() + + @parametrize + async def test_raw_response_stream_stderr_updates(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.executions.with_raw_response.stream_stderr_updates( + execution_id="execution_id", + devbox_id="devbox_id", + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = await response.parse() + await stream.close() + + @parametrize + async def test_streaming_response_stream_stderr_updates(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.executions.with_streaming_response.stream_stderr_updates( + execution_id="execution_id", + devbox_id="devbox_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 + + @parametrize + async def test_path_params_stream_stderr_updates(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `devbox_id` but received ''"): + await async_client.devboxes.executions.with_raw_response.stream_stderr_updates( + execution_id="execution_id", + devbox_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `execution_id` but received ''"): + await async_client.devboxes.executions.with_raw_response.stream_stderr_updates( + execution_id="", + devbox_id="devbox_id", + ) + + @parametrize + async def test_method_stream_stdout_updates(self, async_client: AsyncRunloop) -> None: + execution_stream = await async_client.devboxes.executions.stream_stdout_updates( + execution_id="execution_id", + devbox_id="devbox_id", + ) + await execution_stream.response.aclose() + + @parametrize + async def test_method_stream_stdout_updates_with_all_params(self, async_client: AsyncRunloop) -> None: + execution_stream = await async_client.devboxes.executions.stream_stdout_updates( + execution_id="execution_id", + devbox_id="devbox_id", + offset="offset", + ) + await execution_stream.response.aclose() + + @parametrize + async def test_raw_response_stream_stdout_updates(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.executions.with_raw_response.stream_stdout_updates( + execution_id="execution_id", + devbox_id="devbox_id", + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = await response.parse() + await stream.close() + + @parametrize + async def test_streaming_response_stream_stdout_updates(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.executions.with_streaming_response.stream_stdout_updates( + execution_id="execution_id", + devbox_id="devbox_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 + + @parametrize + async def test_path_params_stream_stdout_updates(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `devbox_id` but received ''"): + await async_client.devboxes.executions.with_raw_response.stream_stdout_updates( + execution_id="execution_id", + devbox_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `execution_id` but received ''"): + await async_client.devboxes.executions.with_raw_response.stream_stdout_updates( + execution_id="", + devbox_id="devbox_id", + ) diff --git a/tests/api_resources/devboxes/test_logs.py b/tests/api_resources/devboxes/test_logs.py new file mode 100644 index 000000000..80f10afca --- /dev/null +++ b/tests/api_resources/devboxes/test_logs.py @@ -0,0 +1,118 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from runloop_api_client import Runloop, AsyncRunloop +from runloop_api_client.types.devboxes import DevboxLogsListView + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestLogs: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_list(self, client: Runloop) -> None: + log = client.devboxes.logs.list( + id="id", + ) + assert_matches_type(DevboxLogsListView, log, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Runloop) -> None: + log = client.devboxes.logs.list( + id="id", + execution_id="execution_id", + shell_name="shell_name", + ) + assert_matches_type(DevboxLogsListView, log, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Runloop) -> None: + response = client.devboxes.logs.with_raw_response.list( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + log = response.parse() + assert_matches_type(DevboxLogsListView, log, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Runloop) -> None: + with client.devboxes.logs.with_streaming_response.list( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + log = response.parse() + assert_matches_type(DevboxLogsListView, log, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_list(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.logs.with_raw_response.list( + id="", + ) + + +class TestAsyncLogs: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_list(self, async_client: AsyncRunloop) -> None: + log = await async_client.devboxes.logs.list( + id="id", + ) + assert_matches_type(DevboxLogsListView, log, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncRunloop) -> None: + log = await async_client.devboxes.logs.list( + id="id", + execution_id="execution_id", + shell_name="shell_name", + ) + assert_matches_type(DevboxLogsListView, log, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.logs.with_raw_response.list( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + log = await response.parse() + assert_matches_type(DevboxLogsListView, log, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.logs.with_streaming_response.list( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + log = await response.parse() + assert_matches_type(DevboxLogsListView, log, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_list(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.logs.with_raw_response.list( + id="", + ) diff --git a/tests/api_resources/scenarios/__init__.py b/tests/api_resources/scenarios/__init__.py new file mode 100644 index 000000000..fd8019a9a --- /dev/null +++ b/tests/api_resources/scenarios/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/scenarios/test_runs.py b/tests/api_resources/scenarios/test_runs.py new file mode 100644 index 000000000..f3ac8eb88 --- /dev/null +++ b/tests/api_resources/scenarios/test_runs.py @@ -0,0 +1,523 @@ +# 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 tests.utils import assert_matches_type +from runloop_api_client import Runloop, AsyncRunloop +from runloop_api_client.types import ScenarioRunView +from runloop_api_client._response import ( + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, +) +from runloop_api_client.pagination import SyncBenchmarkRunsCursorIDPage, AsyncBenchmarkRunsCursorIDPage + +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"]) + + @parametrize + def test_method_retrieve(self, client: Runloop) -> None: + run = client.scenarios.runs.retrieve( + "id", + ) + assert_matches_type(ScenarioRunView, run, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Runloop) -> None: + response = client.scenarios.runs.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + run = response.parse() + assert_matches_type(ScenarioRunView, run, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Runloop) -> None: + with client.scenarios.runs.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + run = response.parse() + assert_matches_type(ScenarioRunView, run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.scenarios.runs.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_list(self, client: Runloop) -> None: + run = client.scenarios.runs.list() + assert_matches_type(SyncBenchmarkRunsCursorIDPage[ScenarioRunView], run, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Runloop) -> None: + run = client.scenarios.runs.list( + benchmark_run_id="benchmark_run_id", + limit=0, + name="name", + scenario_id="scenario_id", + starting_after="starting_after", + state="state", + ) + assert_matches_type(SyncBenchmarkRunsCursorIDPage[ScenarioRunView], run, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Runloop) -> None: + response = client.scenarios.runs.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + run = response.parse() + assert_matches_type(SyncBenchmarkRunsCursorIDPage[ScenarioRunView], run, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Runloop) -> None: + with client.scenarios.runs.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + run = response.parse() + assert_matches_type(SyncBenchmarkRunsCursorIDPage[ScenarioRunView], run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_cancel(self, client: Runloop) -> None: + run = client.scenarios.runs.cancel( + "id", + ) + assert_matches_type(ScenarioRunView, run, path=["response"]) + + @parametrize + def test_raw_response_cancel(self, client: Runloop) -> None: + response = client.scenarios.runs.with_raw_response.cancel( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + run = response.parse() + assert_matches_type(ScenarioRunView, run, path=["response"]) + + @parametrize + def test_streaming_response_cancel(self, client: Runloop) -> None: + with client.scenarios.runs.with_streaming_response.cancel( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + run = response.parse() + assert_matches_type(ScenarioRunView, run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_cancel(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.scenarios.runs.with_raw_response.cancel( + "", + ) + + @parametrize + def test_method_complete(self, client: Runloop) -> None: + run = client.scenarios.runs.complete( + "id", + ) + assert_matches_type(ScenarioRunView, run, path=["response"]) + + @parametrize + def test_raw_response_complete(self, client: Runloop) -> None: + response = client.scenarios.runs.with_raw_response.complete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + run = response.parse() + assert_matches_type(ScenarioRunView, run, path=["response"]) + + @parametrize + def test_streaming_response_complete(self, client: Runloop) -> None: + with client.scenarios.runs.with_streaming_response.complete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + run = response.parse() + assert_matches_type(ScenarioRunView, run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_complete(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.scenarios.runs.with_raw_response.complete( + "", + ) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_method_download_logs(self, client: Runloop, respx_mock: MockRouter) -> None: + respx_mock.post("/v1/scenarios/runs/id/download_logs").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) + run = client.scenarios.runs.download_logs( + "id", + ) + assert run.is_closed + assert run.json() == {"foo": "bar"} + assert cast(Any, run.is_closed) is True + assert isinstance(run, BinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_raw_response_download_logs(self, client: Runloop, respx_mock: MockRouter) -> None: + respx_mock.post("/v1/scenarios/runs/id/download_logs").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) + + run = client.scenarios.runs.with_raw_response.download_logs( + "id", + ) + + assert run.is_closed is True + assert run.http_request.headers.get("X-Stainless-Lang") == "python" + assert run.json() == {"foo": "bar"} + assert isinstance(run, BinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_streaming_response_download_logs(self, client: Runloop, respx_mock: MockRouter) -> None: + respx_mock.post("/v1/scenarios/runs/id/download_logs").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) + with client.scenarios.runs.with_streaming_response.download_logs( + "id", + ) as run: + assert not run.is_closed + assert run.http_request.headers.get("X-Stainless-Lang") == "python" + + assert run.json() == {"foo": "bar"} + assert cast(Any, run.is_closed) is True + assert isinstance(run, StreamedBinaryAPIResponse) + + assert cast(Any, run.is_closed) is True + + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_path_params_download_logs(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.scenarios.runs.with_raw_response.download_logs( + "", + ) + + @parametrize + def test_method_score(self, client: Runloop) -> None: + run = client.scenarios.runs.score( + "id", + ) + assert_matches_type(ScenarioRunView, run, path=["response"]) + + @parametrize + def test_raw_response_score(self, client: Runloop) -> None: + response = client.scenarios.runs.with_raw_response.score( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + run = response.parse() + assert_matches_type(ScenarioRunView, run, path=["response"]) + + @parametrize + def test_streaming_response_score(self, client: Runloop) -> None: + with client.scenarios.runs.with_streaming_response.score( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + run = response.parse() + assert_matches_type(ScenarioRunView, run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_score(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.scenarios.runs.with_raw_response.score( + "", + ) + + +class TestAsyncRuns: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_retrieve(self, async_client: AsyncRunloop) -> None: + run = await async_client.scenarios.runs.retrieve( + "id", + ) + assert_matches_type(ScenarioRunView, run, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncRunloop) -> None: + response = await async_client.scenarios.runs.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + run = await response.parse() + assert_matches_type(ScenarioRunView, run, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncRunloop) -> None: + async with async_client.scenarios.runs.with_streaming_response.retrieve( + "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(ScenarioRunView, run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.scenarios.runs.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncRunloop) -> None: + run = await async_client.scenarios.runs.list() + assert_matches_type(AsyncBenchmarkRunsCursorIDPage[ScenarioRunView], run, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncRunloop) -> None: + run = await async_client.scenarios.runs.list( + benchmark_run_id="benchmark_run_id", + limit=0, + name="name", + scenario_id="scenario_id", + starting_after="starting_after", + state="state", + ) + assert_matches_type(AsyncBenchmarkRunsCursorIDPage[ScenarioRunView], run, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncRunloop) -> None: + response = await async_client.scenarios.runs.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + run = await response.parse() + assert_matches_type(AsyncBenchmarkRunsCursorIDPage[ScenarioRunView], run, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncRunloop) -> None: + async with async_client.scenarios.runs.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + run = await response.parse() + assert_matches_type(AsyncBenchmarkRunsCursorIDPage[ScenarioRunView], run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_cancel(self, async_client: AsyncRunloop) -> None: + run = await async_client.scenarios.runs.cancel( + "id", + ) + assert_matches_type(ScenarioRunView, run, path=["response"]) + + @parametrize + async def test_raw_response_cancel(self, async_client: AsyncRunloop) -> None: + response = await async_client.scenarios.runs.with_raw_response.cancel( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + run = await response.parse() + assert_matches_type(ScenarioRunView, run, path=["response"]) + + @parametrize + async def test_streaming_response_cancel(self, async_client: AsyncRunloop) -> None: + async with async_client.scenarios.runs.with_streaming_response.cancel( + "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(ScenarioRunView, run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_cancel(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.scenarios.runs.with_raw_response.cancel( + "", + ) + + @parametrize + async def test_method_complete(self, async_client: AsyncRunloop) -> None: + run = await async_client.scenarios.runs.complete( + "id", + ) + assert_matches_type(ScenarioRunView, run, path=["response"]) + + @parametrize + async def test_raw_response_complete(self, async_client: AsyncRunloop) -> None: + response = await async_client.scenarios.runs.with_raw_response.complete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + run = await response.parse() + assert_matches_type(ScenarioRunView, run, path=["response"]) + + @parametrize + async def test_streaming_response_complete(self, async_client: AsyncRunloop) -> None: + async with async_client.scenarios.runs.with_streaming_response.complete( + "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(ScenarioRunView, run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_complete(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.scenarios.runs.with_raw_response.complete( + "", + ) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_method_download_logs(self, async_client: AsyncRunloop, respx_mock: MockRouter) -> None: + respx_mock.post("/v1/scenarios/runs/id/download_logs").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) + run = await async_client.scenarios.runs.download_logs( + "id", + ) + assert run.is_closed + assert await run.json() == {"foo": "bar"} + assert cast(Any, run.is_closed) is True + assert isinstance(run, AsyncBinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_raw_response_download_logs(self, async_client: AsyncRunloop, respx_mock: MockRouter) -> None: + respx_mock.post("/v1/scenarios/runs/id/download_logs").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) + + run = await async_client.scenarios.runs.with_raw_response.download_logs( + "id", + ) + + assert run.is_closed is True + assert run.http_request.headers.get("X-Stainless-Lang") == "python" + assert await run.json() == {"foo": "bar"} + assert isinstance(run, AsyncBinaryAPIResponse) + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_streaming_response_download_logs(self, async_client: AsyncRunloop, respx_mock: MockRouter) -> None: + respx_mock.post("/v1/scenarios/runs/id/download_logs").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) + async with async_client.scenarios.runs.with_streaming_response.download_logs( + "id", + ) as run: + assert not run.is_closed + assert run.http_request.headers.get("X-Stainless-Lang") == "python" + + assert await run.json() == {"foo": "bar"} + assert cast(Any, run.is_closed) is True + assert isinstance(run, AsyncStreamedBinaryAPIResponse) + + assert cast(Any, run.is_closed) is True + + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_path_params_download_logs(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.scenarios.runs.with_raw_response.download_logs( + "", + ) + + @parametrize + async def test_method_score(self, async_client: AsyncRunloop) -> None: + run = await async_client.scenarios.runs.score( + "id", + ) + assert_matches_type(ScenarioRunView, run, path=["response"]) + + @parametrize + async def test_raw_response_score(self, async_client: AsyncRunloop) -> None: + response = await async_client.scenarios.runs.with_raw_response.score( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + run = await response.parse() + assert_matches_type(ScenarioRunView, run, path=["response"]) + + @parametrize + async def test_streaming_response_score(self, async_client: AsyncRunloop) -> None: + async with async_client.scenarios.runs.with_streaming_response.score( + "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(ScenarioRunView, run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_score(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.scenarios.runs.with_raw_response.score( + "", + ) diff --git a/tests/api_resources/scenarios/test_scorers.py b/tests/api_resources/scenarios/test_scorers.py new file mode 100644 index 000000000..359e0dcc7 --- /dev/null +++ b/tests/api_resources/scenarios/test_scorers.py @@ -0,0 +1,332 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from runloop_api_client import Runloop, AsyncRunloop +from runloop_api_client.pagination import SyncScenarioScorersCursorIDPage, AsyncScenarioScorersCursorIDPage +from runloop_api_client.types.scenarios import ( + ScorerListResponse, + ScorerCreateResponse, + ScorerUpdateResponse, + ScorerRetrieveResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestScorers: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Runloop) -> None: + scorer = client.scenarios.scorers.create( + bash_script="bash_script", + type="type", + ) + assert_matches_type(ScorerCreateResponse, scorer, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Runloop) -> None: + response = client.scenarios.scorers.with_raw_response.create( + bash_script="bash_script", + type="type", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + scorer = response.parse() + assert_matches_type(ScorerCreateResponse, scorer, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Runloop) -> None: + with client.scenarios.scorers.with_streaming_response.create( + bash_script="bash_script", + type="type", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + scorer = response.parse() + assert_matches_type(ScorerCreateResponse, scorer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_retrieve(self, client: Runloop) -> None: + scorer = client.scenarios.scorers.retrieve( + "id", + ) + assert_matches_type(ScorerRetrieveResponse, scorer, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Runloop) -> None: + response = client.scenarios.scorers.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + scorer = response.parse() + assert_matches_type(ScorerRetrieveResponse, scorer, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Runloop) -> None: + with client.scenarios.scorers.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + scorer = response.parse() + assert_matches_type(ScorerRetrieveResponse, scorer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.scenarios.scorers.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_update(self, client: Runloop) -> None: + scorer = client.scenarios.scorers.update( + id="id", + bash_script="bash_script", + type="type", + ) + assert_matches_type(ScorerUpdateResponse, scorer, path=["response"]) + + @parametrize + def test_raw_response_update(self, client: Runloop) -> None: + response = client.scenarios.scorers.with_raw_response.update( + id="id", + bash_script="bash_script", + type="type", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + scorer = response.parse() + assert_matches_type(ScorerUpdateResponse, scorer, path=["response"]) + + @parametrize + def test_streaming_response_update(self, client: Runloop) -> None: + with client.scenarios.scorers.with_streaming_response.update( + id="id", + bash_script="bash_script", + type="type", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + scorer = response.parse() + assert_matches_type(ScorerUpdateResponse, scorer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_update(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.scenarios.scorers.with_raw_response.update( + id="", + bash_script="bash_script", + type="type", + ) + + @parametrize + def test_method_list(self, client: Runloop) -> None: + scorer = client.scenarios.scorers.list() + assert_matches_type(SyncScenarioScorersCursorIDPage[ScorerListResponse], scorer, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Runloop) -> None: + scorer = client.scenarios.scorers.list( + limit=0, + starting_after="starting_after", + ) + assert_matches_type(SyncScenarioScorersCursorIDPage[ScorerListResponse], scorer, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Runloop) -> None: + response = client.scenarios.scorers.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + scorer = response.parse() + assert_matches_type(SyncScenarioScorersCursorIDPage[ScorerListResponse], scorer, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Runloop) -> None: + with client.scenarios.scorers.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + scorer = response.parse() + assert_matches_type(SyncScenarioScorersCursorIDPage[ScorerListResponse], scorer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncScorers: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_create(self, async_client: AsyncRunloop) -> None: + scorer = await async_client.scenarios.scorers.create( + bash_script="bash_script", + type="type", + ) + assert_matches_type(ScorerCreateResponse, scorer, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncRunloop) -> None: + response = await async_client.scenarios.scorers.with_raw_response.create( + bash_script="bash_script", + type="type", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + scorer = await response.parse() + assert_matches_type(ScorerCreateResponse, scorer, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncRunloop) -> None: + async with async_client.scenarios.scorers.with_streaming_response.create( + bash_script="bash_script", + type="type", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + scorer = await response.parse() + assert_matches_type(ScorerCreateResponse, scorer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_retrieve(self, async_client: AsyncRunloop) -> None: + scorer = await async_client.scenarios.scorers.retrieve( + "id", + ) + assert_matches_type(ScorerRetrieveResponse, scorer, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncRunloop) -> None: + response = await async_client.scenarios.scorers.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + scorer = await response.parse() + assert_matches_type(ScorerRetrieveResponse, scorer, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncRunloop) -> None: + async with async_client.scenarios.scorers.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + scorer = await response.parse() + assert_matches_type(ScorerRetrieveResponse, scorer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.scenarios.scorers.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_update(self, async_client: AsyncRunloop) -> None: + scorer = await async_client.scenarios.scorers.update( + id="id", + bash_script="bash_script", + type="type", + ) + assert_matches_type(ScorerUpdateResponse, scorer, path=["response"]) + + @parametrize + async def test_raw_response_update(self, async_client: AsyncRunloop) -> None: + response = await async_client.scenarios.scorers.with_raw_response.update( + id="id", + bash_script="bash_script", + type="type", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + scorer = await response.parse() + assert_matches_type(ScorerUpdateResponse, scorer, path=["response"]) + + @parametrize + async def test_streaming_response_update(self, async_client: AsyncRunloop) -> None: + async with async_client.scenarios.scorers.with_streaming_response.update( + id="id", + bash_script="bash_script", + type="type", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + scorer = await response.parse() + assert_matches_type(ScorerUpdateResponse, scorer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_update(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.scenarios.scorers.with_raw_response.update( + id="", + bash_script="bash_script", + type="type", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncRunloop) -> None: + scorer = await async_client.scenarios.scorers.list() + assert_matches_type(AsyncScenarioScorersCursorIDPage[ScorerListResponse], scorer, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncRunloop) -> None: + scorer = await async_client.scenarios.scorers.list( + limit=0, + starting_after="starting_after", + ) + assert_matches_type(AsyncScenarioScorersCursorIDPage[ScorerListResponse], scorer, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncRunloop) -> None: + response = await async_client.scenarios.scorers.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + scorer = await response.parse() + assert_matches_type(AsyncScenarioScorersCursorIDPage[ScorerListResponse], scorer, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncRunloop) -> None: + async with async_client.scenarios.scorers.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + scorer = await response.parse() + assert_matches_type(AsyncScenarioScorersCursorIDPage[ScorerListResponse], scorer, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_agents.py b/tests/api_resources/test_agents.py new file mode 100644 index 000000000..693eec250 --- /dev/null +++ b/tests/api_resources/test_agents.py @@ -0,0 +1,303 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from runloop_api_client import Runloop, AsyncRunloop +from runloop_api_client.types import AgentView +from runloop_api_client.pagination import SyncAgentsCursorIDPage, AsyncAgentsCursorIDPage + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestAgents: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Runloop) -> None: + agent = client.agents.create( + name="name", + version="version", + ) + assert_matches_type(AgentView, agent, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: Runloop) -> None: + agent = client.agents.create( + name="name", + version="version", + source={ + "type": "type", + "git": { + "repository": "repository", + "agent_setup": ["string"], + "ref": "ref", + }, + "npm": { + "package_name": "package_name", + "agent_setup": ["string"], + "registry_url": "registry_url", + }, + "object": { + "object_id": "object_id", + "agent_setup": ["string"], + }, + "pip": { + "package_name": "package_name", + "agent_setup": ["string"], + "registry_url": "registry_url", + }, + }, + ) + assert_matches_type(AgentView, agent, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Runloop) -> None: + response = client.agents.with_raw_response.create( + name="name", + version="version", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + agent = response.parse() + assert_matches_type(AgentView, agent, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Runloop) -> None: + with client.agents.with_streaming_response.create( + name="name", + version="version", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + agent = response.parse() + assert_matches_type(AgentView, agent, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_retrieve(self, client: Runloop) -> None: + agent = client.agents.retrieve( + "id", + ) + assert_matches_type(AgentView, agent, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Runloop) -> None: + response = client.agents.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + agent = response.parse() + assert_matches_type(AgentView, agent, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Runloop) -> None: + with client.agents.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + agent = response.parse() + assert_matches_type(AgentView, agent, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.agents.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_list(self, client: Runloop) -> None: + agent = client.agents.list() + assert_matches_type(SyncAgentsCursorIDPage[AgentView], agent, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Runloop) -> None: + agent = client.agents.list( + is_public=True, + limit=0, + name="name", + search="search", + starting_after="starting_after", + version="version", + ) + assert_matches_type(SyncAgentsCursorIDPage[AgentView], agent, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Runloop) -> None: + response = client.agents.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + agent = response.parse() + assert_matches_type(SyncAgentsCursorIDPage[AgentView], agent, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Runloop) -> None: + with client.agents.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + agent = response.parse() + assert_matches_type(SyncAgentsCursorIDPage[AgentView], agent, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncAgents: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_create(self, async_client: AsyncRunloop) -> None: + agent = await async_client.agents.create( + name="name", + version="version", + ) + assert_matches_type(AgentView, agent, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncRunloop) -> None: + agent = await async_client.agents.create( + name="name", + version="version", + source={ + "type": "type", + "git": { + "repository": "repository", + "agent_setup": ["string"], + "ref": "ref", + }, + "npm": { + "package_name": "package_name", + "agent_setup": ["string"], + "registry_url": "registry_url", + }, + "object": { + "object_id": "object_id", + "agent_setup": ["string"], + }, + "pip": { + "package_name": "package_name", + "agent_setup": ["string"], + "registry_url": "registry_url", + }, + }, + ) + assert_matches_type(AgentView, agent, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncRunloop) -> None: + response = await async_client.agents.with_raw_response.create( + name="name", + version="version", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + agent = await response.parse() + assert_matches_type(AgentView, agent, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncRunloop) -> None: + async with async_client.agents.with_streaming_response.create( + name="name", + version="version", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + agent = await response.parse() + assert_matches_type(AgentView, agent, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_retrieve(self, async_client: AsyncRunloop) -> None: + agent = await async_client.agents.retrieve( + "id", + ) + assert_matches_type(AgentView, agent, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncRunloop) -> None: + response = await async_client.agents.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + agent = await response.parse() + assert_matches_type(AgentView, agent, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncRunloop) -> None: + async with async_client.agents.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + agent = await response.parse() + assert_matches_type(AgentView, agent, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.agents.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncRunloop) -> None: + agent = await async_client.agents.list() + assert_matches_type(AsyncAgentsCursorIDPage[AgentView], agent, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncRunloop) -> None: + agent = await async_client.agents.list( + is_public=True, + limit=0, + name="name", + search="search", + starting_after="starting_after", + version="version", + ) + assert_matches_type(AsyncAgentsCursorIDPage[AgentView], agent, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncRunloop) -> None: + response = await async_client.agents.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + agent = await response.parse() + assert_matches_type(AsyncAgentsCursorIDPage[AgentView], agent, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncRunloop) -> None: + async with async_client.agents.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + agent = await response.parse() + assert_matches_type(AsyncAgentsCursorIDPage[AgentView], agent, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_benchmark_jobs.py b/tests/api_resources/test_benchmark_jobs.py new file mode 100644 index 000000000..461943458 --- /dev/null +++ b/tests/api_resources/test_benchmark_jobs.py @@ -0,0 +1,243 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from runloop_api_client import Runloop, AsyncRunloop +from runloop_api_client.types import ( + BenchmarkJobView, + BenchmarkJobListView, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestBenchmarkJobs: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Runloop) -> None: + benchmark_job = client.benchmark_jobs.create() + assert_matches_type(BenchmarkJobView, benchmark_job, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: Runloop) -> None: + benchmark_job = client.benchmark_jobs.create( + name="name", + spec={ + "inline_yaml": "inline_yaml", + "type": "harbor", + }, + ) + assert_matches_type(BenchmarkJobView, benchmark_job, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Runloop) -> None: + response = client.benchmark_jobs.with_raw_response.create() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benchmark_job = response.parse() + assert_matches_type(BenchmarkJobView, benchmark_job, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Runloop) -> None: + with client.benchmark_jobs.with_streaming_response.create() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + benchmark_job = response.parse() + assert_matches_type(BenchmarkJobView, benchmark_job, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_retrieve(self, client: Runloop) -> None: + benchmark_job = client.benchmark_jobs.retrieve( + "id", + ) + assert_matches_type(BenchmarkJobView, benchmark_job, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Runloop) -> None: + response = client.benchmark_jobs.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benchmark_job = response.parse() + assert_matches_type(BenchmarkJobView, benchmark_job, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Runloop) -> None: + with client.benchmark_jobs.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + benchmark_job = response.parse() + assert_matches_type(BenchmarkJobView, benchmark_job, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.benchmark_jobs.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_list(self, client: Runloop) -> None: + benchmark_job = client.benchmark_jobs.list() + assert_matches_type(BenchmarkJobListView, benchmark_job, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Runloop) -> None: + benchmark_job = client.benchmark_jobs.list( + limit=0, + name="name", + starting_after="starting_after", + ) + assert_matches_type(BenchmarkJobListView, benchmark_job, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Runloop) -> None: + response = client.benchmark_jobs.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benchmark_job = response.parse() + assert_matches_type(BenchmarkJobListView, benchmark_job, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Runloop) -> None: + with client.benchmark_jobs.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + benchmark_job = response.parse() + assert_matches_type(BenchmarkJobListView, benchmark_job, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncBenchmarkJobs: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_create(self, async_client: AsyncRunloop) -> None: + benchmark_job = await async_client.benchmark_jobs.create() + assert_matches_type(BenchmarkJobView, benchmark_job, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncRunloop) -> None: + benchmark_job = await async_client.benchmark_jobs.create( + name="name", + spec={ + "inline_yaml": "inline_yaml", + "type": "harbor", + }, + ) + assert_matches_type(BenchmarkJobView, benchmark_job, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncRunloop) -> None: + response = await async_client.benchmark_jobs.with_raw_response.create() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benchmark_job = await response.parse() + assert_matches_type(BenchmarkJobView, benchmark_job, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncRunloop) -> None: + async with async_client.benchmark_jobs.with_streaming_response.create() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + benchmark_job = await response.parse() + assert_matches_type(BenchmarkJobView, benchmark_job, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_retrieve(self, async_client: AsyncRunloop) -> None: + benchmark_job = await async_client.benchmark_jobs.retrieve( + "id", + ) + assert_matches_type(BenchmarkJobView, benchmark_job, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncRunloop) -> None: + response = await async_client.benchmark_jobs.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benchmark_job = await response.parse() + assert_matches_type(BenchmarkJobView, benchmark_job, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncRunloop) -> None: + async with async_client.benchmark_jobs.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + benchmark_job = await response.parse() + assert_matches_type(BenchmarkJobView, benchmark_job, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.benchmark_jobs.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncRunloop) -> None: + benchmark_job = await async_client.benchmark_jobs.list() + assert_matches_type(BenchmarkJobListView, benchmark_job, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncRunloop) -> None: + benchmark_job = await async_client.benchmark_jobs.list( + limit=0, + name="name", + starting_after="starting_after", + ) + assert_matches_type(BenchmarkJobListView, benchmark_job, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncRunloop) -> None: + response = await async_client.benchmark_jobs.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benchmark_job = await response.parse() + assert_matches_type(BenchmarkJobListView, benchmark_job, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncRunloop) -> None: + async with async_client.benchmark_jobs.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + benchmark_job = await response.parse() + assert_matches_type(BenchmarkJobListView, benchmark_job, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_benchmark_runs.py b/tests/api_resources/test_benchmark_runs.py new file mode 100644 index 000000000..854a44574 --- /dev/null +++ b/tests/api_resources/test_benchmark_runs.py @@ -0,0 +1,422 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from runloop_api_client import Runloop, AsyncRunloop +from runloop_api_client.types import ( + ScenarioRunView, + BenchmarkRunView, +) +from runloop_api_client.pagination import SyncBenchmarkRunsCursorIDPage, AsyncBenchmarkRunsCursorIDPage + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestBenchmarkRuns: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_retrieve(self, client: Runloop) -> None: + benchmark_run = client.benchmark_runs.retrieve( + "id", + ) + assert_matches_type(BenchmarkRunView, benchmark_run, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Runloop) -> None: + response = client.benchmark_runs.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benchmark_run = response.parse() + assert_matches_type(BenchmarkRunView, benchmark_run, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Runloop) -> None: + with client.benchmark_runs.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + benchmark_run = response.parse() + assert_matches_type(BenchmarkRunView, benchmark_run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.benchmark_runs.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_list(self, client: Runloop) -> None: + benchmark_run = client.benchmark_runs.list() + assert_matches_type(SyncBenchmarkRunsCursorIDPage[BenchmarkRunView], benchmark_run, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Runloop) -> None: + benchmark_run = client.benchmark_runs.list( + benchmark_id="benchmark_id", + limit=0, + name="name", + starting_after="starting_after", + ) + assert_matches_type(SyncBenchmarkRunsCursorIDPage[BenchmarkRunView], benchmark_run, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Runloop) -> None: + response = client.benchmark_runs.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benchmark_run = response.parse() + assert_matches_type(SyncBenchmarkRunsCursorIDPage[BenchmarkRunView], benchmark_run, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Runloop) -> None: + with client.benchmark_runs.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + benchmark_run = response.parse() + assert_matches_type(SyncBenchmarkRunsCursorIDPage[BenchmarkRunView], benchmark_run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_cancel(self, client: Runloop) -> None: + benchmark_run = client.benchmark_runs.cancel( + "id", + ) + assert_matches_type(BenchmarkRunView, benchmark_run, path=["response"]) + + @parametrize + def test_raw_response_cancel(self, client: Runloop) -> None: + response = client.benchmark_runs.with_raw_response.cancel( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benchmark_run = response.parse() + assert_matches_type(BenchmarkRunView, benchmark_run, path=["response"]) + + @parametrize + def test_streaming_response_cancel(self, client: Runloop) -> None: + with client.benchmark_runs.with_streaming_response.cancel( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + benchmark_run = response.parse() + assert_matches_type(BenchmarkRunView, benchmark_run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_cancel(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.benchmark_runs.with_raw_response.cancel( + "", + ) + + @parametrize + def test_method_complete(self, client: Runloop) -> None: + benchmark_run = client.benchmark_runs.complete( + "id", + ) + assert_matches_type(BenchmarkRunView, benchmark_run, path=["response"]) + + @parametrize + def test_raw_response_complete(self, client: Runloop) -> None: + response = client.benchmark_runs.with_raw_response.complete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benchmark_run = response.parse() + assert_matches_type(BenchmarkRunView, benchmark_run, path=["response"]) + + @parametrize + def test_streaming_response_complete(self, client: Runloop) -> None: + with client.benchmark_runs.with_streaming_response.complete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + benchmark_run = response.parse() + assert_matches_type(BenchmarkRunView, benchmark_run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_complete(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.benchmark_runs.with_raw_response.complete( + "", + ) + + @parametrize + def test_method_list_scenario_runs(self, client: Runloop) -> None: + benchmark_run = client.benchmark_runs.list_scenario_runs( + id="id", + ) + assert_matches_type(SyncBenchmarkRunsCursorIDPage[ScenarioRunView], benchmark_run, path=["response"]) + + @parametrize + def test_method_list_scenario_runs_with_all_params(self, client: Runloop) -> None: + benchmark_run = client.benchmark_runs.list_scenario_runs( + id="id", + limit=0, + starting_after="starting_after", + state="running", + ) + assert_matches_type(SyncBenchmarkRunsCursorIDPage[ScenarioRunView], benchmark_run, path=["response"]) + + @parametrize + def test_raw_response_list_scenario_runs(self, client: Runloop) -> None: + response = client.benchmark_runs.with_raw_response.list_scenario_runs( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benchmark_run = response.parse() + assert_matches_type(SyncBenchmarkRunsCursorIDPage[ScenarioRunView], benchmark_run, path=["response"]) + + @parametrize + def test_streaming_response_list_scenario_runs(self, client: Runloop) -> None: + with client.benchmark_runs.with_streaming_response.list_scenario_runs( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + benchmark_run = response.parse() + assert_matches_type(SyncBenchmarkRunsCursorIDPage[ScenarioRunView], benchmark_run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_list_scenario_runs(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.benchmark_runs.with_raw_response.list_scenario_runs( + id="", + ) + + +class TestAsyncBenchmarkRuns: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_retrieve(self, async_client: AsyncRunloop) -> None: + benchmark_run = await async_client.benchmark_runs.retrieve( + "id", + ) + assert_matches_type(BenchmarkRunView, benchmark_run, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncRunloop) -> None: + response = await async_client.benchmark_runs.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benchmark_run = await response.parse() + assert_matches_type(BenchmarkRunView, benchmark_run, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncRunloop) -> None: + async with async_client.benchmark_runs.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + benchmark_run = await response.parse() + assert_matches_type(BenchmarkRunView, benchmark_run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.benchmark_runs.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncRunloop) -> None: + benchmark_run = await async_client.benchmark_runs.list() + assert_matches_type(AsyncBenchmarkRunsCursorIDPage[BenchmarkRunView], benchmark_run, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncRunloop) -> None: + benchmark_run = await async_client.benchmark_runs.list( + benchmark_id="benchmark_id", + limit=0, + name="name", + starting_after="starting_after", + ) + assert_matches_type(AsyncBenchmarkRunsCursorIDPage[BenchmarkRunView], benchmark_run, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncRunloop) -> None: + response = await async_client.benchmark_runs.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benchmark_run = await response.parse() + assert_matches_type(AsyncBenchmarkRunsCursorIDPage[BenchmarkRunView], benchmark_run, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncRunloop) -> None: + async with async_client.benchmark_runs.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + benchmark_run = await response.parse() + assert_matches_type(AsyncBenchmarkRunsCursorIDPage[BenchmarkRunView], benchmark_run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_cancel(self, async_client: AsyncRunloop) -> None: + benchmark_run = await async_client.benchmark_runs.cancel( + "id", + ) + assert_matches_type(BenchmarkRunView, benchmark_run, path=["response"]) + + @parametrize + async def test_raw_response_cancel(self, async_client: AsyncRunloop) -> None: + response = await async_client.benchmark_runs.with_raw_response.cancel( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benchmark_run = await response.parse() + assert_matches_type(BenchmarkRunView, benchmark_run, path=["response"]) + + @parametrize + async def test_streaming_response_cancel(self, async_client: AsyncRunloop) -> None: + async with async_client.benchmark_runs.with_streaming_response.cancel( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + benchmark_run = await response.parse() + assert_matches_type(BenchmarkRunView, benchmark_run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_cancel(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.benchmark_runs.with_raw_response.cancel( + "", + ) + + @parametrize + async def test_method_complete(self, async_client: AsyncRunloop) -> None: + benchmark_run = await async_client.benchmark_runs.complete( + "id", + ) + assert_matches_type(BenchmarkRunView, benchmark_run, path=["response"]) + + @parametrize + async def test_raw_response_complete(self, async_client: AsyncRunloop) -> None: + response = await async_client.benchmark_runs.with_raw_response.complete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benchmark_run = await response.parse() + assert_matches_type(BenchmarkRunView, benchmark_run, path=["response"]) + + @parametrize + async def test_streaming_response_complete(self, async_client: AsyncRunloop) -> None: + async with async_client.benchmark_runs.with_streaming_response.complete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + benchmark_run = await response.parse() + assert_matches_type(BenchmarkRunView, benchmark_run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_complete(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.benchmark_runs.with_raw_response.complete( + "", + ) + + @parametrize + async def test_method_list_scenario_runs(self, async_client: AsyncRunloop) -> None: + benchmark_run = await async_client.benchmark_runs.list_scenario_runs( + id="id", + ) + assert_matches_type(AsyncBenchmarkRunsCursorIDPage[ScenarioRunView], benchmark_run, path=["response"]) + + @parametrize + async def test_method_list_scenario_runs_with_all_params(self, async_client: AsyncRunloop) -> None: + benchmark_run = await async_client.benchmark_runs.list_scenario_runs( + id="id", + limit=0, + starting_after="starting_after", + state="running", + ) + assert_matches_type(AsyncBenchmarkRunsCursorIDPage[ScenarioRunView], benchmark_run, path=["response"]) + + @parametrize + async def test_raw_response_list_scenario_runs(self, async_client: AsyncRunloop) -> None: + response = await async_client.benchmark_runs.with_raw_response.list_scenario_runs( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benchmark_run = await response.parse() + assert_matches_type(AsyncBenchmarkRunsCursorIDPage[ScenarioRunView], benchmark_run, path=["response"]) + + @parametrize + async def test_streaming_response_list_scenario_runs(self, async_client: AsyncRunloop) -> None: + async with async_client.benchmark_runs.with_streaming_response.list_scenario_runs( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + benchmark_run = await response.parse() + assert_matches_type(AsyncBenchmarkRunsCursorIDPage[ScenarioRunView], benchmark_run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_list_scenario_runs(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.benchmark_runs.with_raw_response.list_scenario_runs( + id="", + ) diff --git a/tests/api_resources/test_benchmarks.py b/tests/api_resources/test_benchmarks.py new file mode 100644 index 000000000..803c75c60 --- /dev/null +++ b/tests/api_resources/test_benchmarks.py @@ -0,0 +1,763 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from runloop_api_client import Runloop, AsyncRunloop +from runloop_api_client.types import ( + BenchmarkView, + BenchmarkRunView, + ScenarioDefinitionListView, +) +from runloop_api_client.pagination import SyncBenchmarksCursorIDPage, AsyncBenchmarksCursorIDPage + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestBenchmarks: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Runloop) -> None: + benchmark = client.benchmarks.create( + name="name", + ) + assert_matches_type(BenchmarkView, benchmark, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: Runloop) -> None: + benchmark = client.benchmarks.create( + name="name", + attribution="attribution", + description="description", + metadata={"foo": "string"}, + required_environment_variables=["string"], + required_secret_names=["string"], + scenario_ids=["string"], + ) + assert_matches_type(BenchmarkView, benchmark, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Runloop) -> None: + response = client.benchmarks.with_raw_response.create( + name="name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benchmark = response.parse() + assert_matches_type(BenchmarkView, benchmark, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Runloop) -> None: + with client.benchmarks.with_streaming_response.create( + name="name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + benchmark = response.parse() + assert_matches_type(BenchmarkView, benchmark, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_retrieve(self, client: Runloop) -> None: + benchmark = client.benchmarks.retrieve( + "id", + ) + assert_matches_type(BenchmarkView, benchmark, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Runloop) -> None: + response = client.benchmarks.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benchmark = response.parse() + assert_matches_type(BenchmarkView, benchmark, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Runloop) -> None: + with client.benchmarks.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + benchmark = response.parse() + assert_matches_type(BenchmarkView, benchmark, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.benchmarks.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_update(self, client: Runloop) -> None: + benchmark = client.benchmarks.update( + id="id", + ) + assert_matches_type(BenchmarkView, benchmark, path=["response"]) + + @parametrize + def test_method_update_with_all_params(self, client: Runloop) -> None: + benchmark = client.benchmarks.update( + id="id", + attribution="attribution", + description="description", + metadata={"foo": "string"}, + name="name", + required_environment_variables=["string"], + required_secret_names=["string"], + scenario_ids=["string"], + ) + assert_matches_type(BenchmarkView, benchmark, path=["response"]) + + @parametrize + def test_raw_response_update(self, client: Runloop) -> None: + response = client.benchmarks.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benchmark = response.parse() + assert_matches_type(BenchmarkView, benchmark, path=["response"]) + + @parametrize + def test_streaming_response_update(self, client: Runloop) -> None: + with client.benchmarks.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + benchmark = response.parse() + assert_matches_type(BenchmarkView, benchmark, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_update(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.benchmarks.with_raw_response.update( + id="", + ) + + @parametrize + def test_method_list(self, client: Runloop) -> None: + benchmark = client.benchmarks.list() + assert_matches_type(SyncBenchmarksCursorIDPage[BenchmarkView], benchmark, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Runloop) -> None: + benchmark = client.benchmarks.list( + limit=0, + name="name", + starting_after="starting_after", + ) + assert_matches_type(SyncBenchmarksCursorIDPage[BenchmarkView], benchmark, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Runloop) -> None: + response = client.benchmarks.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benchmark = response.parse() + assert_matches_type(SyncBenchmarksCursorIDPage[BenchmarkView], benchmark, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Runloop) -> None: + with client.benchmarks.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + benchmark = response.parse() + assert_matches_type(SyncBenchmarksCursorIDPage[BenchmarkView], benchmark, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_definitions(self, client: Runloop) -> None: + benchmark = client.benchmarks.definitions( + id="id", + ) + assert_matches_type(ScenarioDefinitionListView, benchmark, path=["response"]) + + @parametrize + def test_method_definitions_with_all_params(self, client: Runloop) -> None: + benchmark = client.benchmarks.definitions( + id="id", + limit=0, + starting_after="starting_after", + ) + assert_matches_type(ScenarioDefinitionListView, benchmark, path=["response"]) + + @parametrize + def test_raw_response_definitions(self, client: Runloop) -> None: + response = client.benchmarks.with_raw_response.definitions( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benchmark = response.parse() + assert_matches_type(ScenarioDefinitionListView, benchmark, path=["response"]) + + @parametrize + def test_streaming_response_definitions(self, client: Runloop) -> None: + with client.benchmarks.with_streaming_response.definitions( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + benchmark = response.parse() + assert_matches_type(ScenarioDefinitionListView, benchmark, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_definitions(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.benchmarks.with_raw_response.definitions( + id="", + ) + + @parametrize + def test_method_list_public(self, client: Runloop) -> None: + benchmark = client.benchmarks.list_public() + assert_matches_type(SyncBenchmarksCursorIDPage[BenchmarkView], benchmark, path=["response"]) + + @parametrize + def test_method_list_public_with_all_params(self, client: Runloop) -> None: + benchmark = client.benchmarks.list_public( + limit=0, + starting_after="starting_after", + ) + assert_matches_type(SyncBenchmarksCursorIDPage[BenchmarkView], benchmark, path=["response"]) + + @parametrize + def test_raw_response_list_public(self, client: Runloop) -> None: + response = client.benchmarks.with_raw_response.list_public() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benchmark = response.parse() + assert_matches_type(SyncBenchmarksCursorIDPage[BenchmarkView], benchmark, path=["response"]) + + @parametrize + def test_streaming_response_list_public(self, client: Runloop) -> None: + with client.benchmarks.with_streaming_response.list_public() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + benchmark = response.parse() + assert_matches_type(SyncBenchmarksCursorIDPage[BenchmarkView], benchmark, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_start_run(self, client: Runloop) -> None: + benchmark = client.benchmarks.start_run( + benchmark_id="benchmark_id", + ) + assert_matches_type(BenchmarkRunView, benchmark, path=["response"]) + + @parametrize + def test_method_start_run_with_all_params(self, client: Runloop) -> None: + benchmark = client.benchmarks.start_run( + benchmark_id="benchmark_id", + metadata={"foo": "string"}, + run_name="run_name", + run_profile={ + "env_vars": {"foo": "string"}, + "launch_parameters": { + "after_idle": { + "idle_time_seconds": 0, + "on_idle": "shutdown", + }, + "architecture": "x86_64", + "available_ports": [0], + "custom_cpu_cores": 0, + "custom_disk_size": 0, + "custom_gb_memory": 0, + "keep_alive_time_seconds": 0, + "launch_commands": ["string"], + "network_policy_id": "network_policy_id", + "required_services": ["string"], + "resource_size_request": "X_SMALL", + "user_parameters": { + "uid": 0, + "username": "username", + }, + }, + "mounts": [ + { + "object_id": "object_id", + "object_path": "object_path", + "type": "object_mount", + } + ], + "purpose": "purpose", + "secrets": {"foo": "string"}, + }, + ) + assert_matches_type(BenchmarkRunView, benchmark, path=["response"]) + + @parametrize + def test_raw_response_start_run(self, client: Runloop) -> None: + response = client.benchmarks.with_raw_response.start_run( + benchmark_id="benchmark_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benchmark = response.parse() + assert_matches_type(BenchmarkRunView, benchmark, path=["response"]) + + @parametrize + def test_streaming_response_start_run(self, client: Runloop) -> None: + with client.benchmarks.with_streaming_response.start_run( + benchmark_id="benchmark_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + benchmark = response.parse() + assert_matches_type(BenchmarkRunView, benchmark, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_update_scenarios(self, client: Runloop) -> None: + benchmark = client.benchmarks.update_scenarios( + id="id", + ) + assert_matches_type(BenchmarkView, benchmark, path=["response"]) + + @parametrize + def test_method_update_scenarios_with_all_params(self, client: Runloop) -> None: + benchmark = client.benchmarks.update_scenarios( + id="id", + scenarios_to_add=["string"], + scenarios_to_remove=["string"], + ) + assert_matches_type(BenchmarkView, benchmark, path=["response"]) + + @parametrize + def test_raw_response_update_scenarios(self, client: Runloop) -> None: + response = client.benchmarks.with_raw_response.update_scenarios( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benchmark = response.parse() + assert_matches_type(BenchmarkView, benchmark, path=["response"]) + + @parametrize + def test_streaming_response_update_scenarios(self, client: Runloop) -> None: + with client.benchmarks.with_streaming_response.update_scenarios( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + benchmark = response.parse() + assert_matches_type(BenchmarkView, benchmark, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_update_scenarios(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.benchmarks.with_raw_response.update_scenarios( + id="", + ) + + +class TestAsyncBenchmarks: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_create(self, async_client: AsyncRunloop) -> None: + benchmark = await async_client.benchmarks.create( + name="name", + ) + assert_matches_type(BenchmarkView, benchmark, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncRunloop) -> None: + benchmark = await async_client.benchmarks.create( + name="name", + attribution="attribution", + description="description", + metadata={"foo": "string"}, + required_environment_variables=["string"], + required_secret_names=["string"], + scenario_ids=["string"], + ) + assert_matches_type(BenchmarkView, benchmark, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncRunloop) -> None: + response = await async_client.benchmarks.with_raw_response.create( + name="name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benchmark = await response.parse() + assert_matches_type(BenchmarkView, benchmark, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncRunloop) -> None: + async with async_client.benchmarks.with_streaming_response.create( + name="name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + benchmark = await response.parse() + assert_matches_type(BenchmarkView, benchmark, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_retrieve(self, async_client: AsyncRunloop) -> None: + benchmark = await async_client.benchmarks.retrieve( + "id", + ) + assert_matches_type(BenchmarkView, benchmark, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncRunloop) -> None: + response = await async_client.benchmarks.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benchmark = await response.parse() + assert_matches_type(BenchmarkView, benchmark, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncRunloop) -> None: + async with async_client.benchmarks.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + benchmark = await response.parse() + assert_matches_type(BenchmarkView, benchmark, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.benchmarks.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_update(self, async_client: AsyncRunloop) -> None: + benchmark = await async_client.benchmarks.update( + id="id", + ) + assert_matches_type(BenchmarkView, benchmark, path=["response"]) + + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncRunloop) -> None: + benchmark = await async_client.benchmarks.update( + id="id", + attribution="attribution", + description="description", + metadata={"foo": "string"}, + name="name", + required_environment_variables=["string"], + required_secret_names=["string"], + scenario_ids=["string"], + ) + assert_matches_type(BenchmarkView, benchmark, path=["response"]) + + @parametrize + async def test_raw_response_update(self, async_client: AsyncRunloop) -> None: + response = await async_client.benchmarks.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benchmark = await response.parse() + assert_matches_type(BenchmarkView, benchmark, path=["response"]) + + @parametrize + async def test_streaming_response_update(self, async_client: AsyncRunloop) -> None: + async with async_client.benchmarks.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + benchmark = await response.parse() + assert_matches_type(BenchmarkView, benchmark, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_update(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.benchmarks.with_raw_response.update( + id="", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncRunloop) -> None: + benchmark = await async_client.benchmarks.list() + assert_matches_type(AsyncBenchmarksCursorIDPage[BenchmarkView], benchmark, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncRunloop) -> None: + benchmark = await async_client.benchmarks.list( + limit=0, + name="name", + starting_after="starting_after", + ) + assert_matches_type(AsyncBenchmarksCursorIDPage[BenchmarkView], benchmark, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncRunloop) -> None: + response = await async_client.benchmarks.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benchmark = await response.parse() + assert_matches_type(AsyncBenchmarksCursorIDPage[BenchmarkView], benchmark, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncRunloop) -> None: + async with async_client.benchmarks.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + benchmark = await response.parse() + assert_matches_type(AsyncBenchmarksCursorIDPage[BenchmarkView], benchmark, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_definitions(self, async_client: AsyncRunloop) -> None: + benchmark = await async_client.benchmarks.definitions( + id="id", + ) + assert_matches_type(ScenarioDefinitionListView, benchmark, path=["response"]) + + @parametrize + async def test_method_definitions_with_all_params(self, async_client: AsyncRunloop) -> None: + benchmark = await async_client.benchmarks.definitions( + id="id", + limit=0, + starting_after="starting_after", + ) + assert_matches_type(ScenarioDefinitionListView, benchmark, path=["response"]) + + @parametrize + async def test_raw_response_definitions(self, async_client: AsyncRunloop) -> None: + response = await async_client.benchmarks.with_raw_response.definitions( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benchmark = await response.parse() + assert_matches_type(ScenarioDefinitionListView, benchmark, path=["response"]) + + @parametrize + async def test_streaming_response_definitions(self, async_client: AsyncRunloop) -> None: + async with async_client.benchmarks.with_streaming_response.definitions( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + benchmark = await response.parse() + assert_matches_type(ScenarioDefinitionListView, benchmark, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_definitions(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.benchmarks.with_raw_response.definitions( + id="", + ) + + @parametrize + async def test_method_list_public(self, async_client: AsyncRunloop) -> None: + benchmark = await async_client.benchmarks.list_public() + assert_matches_type(AsyncBenchmarksCursorIDPage[BenchmarkView], benchmark, path=["response"]) + + @parametrize + async def test_method_list_public_with_all_params(self, async_client: AsyncRunloop) -> None: + benchmark = await async_client.benchmarks.list_public( + limit=0, + starting_after="starting_after", + ) + assert_matches_type(AsyncBenchmarksCursorIDPage[BenchmarkView], benchmark, path=["response"]) + + @parametrize + async def test_raw_response_list_public(self, async_client: AsyncRunloop) -> None: + response = await async_client.benchmarks.with_raw_response.list_public() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benchmark = await response.parse() + assert_matches_type(AsyncBenchmarksCursorIDPage[BenchmarkView], benchmark, path=["response"]) + + @parametrize + async def test_streaming_response_list_public(self, async_client: AsyncRunloop) -> None: + async with async_client.benchmarks.with_streaming_response.list_public() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + benchmark = await response.parse() + assert_matches_type(AsyncBenchmarksCursorIDPage[BenchmarkView], benchmark, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_start_run(self, async_client: AsyncRunloop) -> None: + benchmark = await async_client.benchmarks.start_run( + benchmark_id="benchmark_id", + ) + assert_matches_type(BenchmarkRunView, benchmark, path=["response"]) + + @parametrize + async def test_method_start_run_with_all_params(self, async_client: AsyncRunloop) -> None: + benchmark = await async_client.benchmarks.start_run( + benchmark_id="benchmark_id", + metadata={"foo": "string"}, + run_name="run_name", + run_profile={ + "env_vars": {"foo": "string"}, + "launch_parameters": { + "after_idle": { + "idle_time_seconds": 0, + "on_idle": "shutdown", + }, + "architecture": "x86_64", + "available_ports": [0], + "custom_cpu_cores": 0, + "custom_disk_size": 0, + "custom_gb_memory": 0, + "keep_alive_time_seconds": 0, + "launch_commands": ["string"], + "network_policy_id": "network_policy_id", + "required_services": ["string"], + "resource_size_request": "X_SMALL", + "user_parameters": { + "uid": 0, + "username": "username", + }, + }, + "mounts": [ + { + "object_id": "object_id", + "object_path": "object_path", + "type": "object_mount", + } + ], + "purpose": "purpose", + "secrets": {"foo": "string"}, + }, + ) + assert_matches_type(BenchmarkRunView, benchmark, path=["response"]) + + @parametrize + async def test_raw_response_start_run(self, async_client: AsyncRunloop) -> None: + response = await async_client.benchmarks.with_raw_response.start_run( + benchmark_id="benchmark_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benchmark = await response.parse() + assert_matches_type(BenchmarkRunView, benchmark, path=["response"]) + + @parametrize + async def test_streaming_response_start_run(self, async_client: AsyncRunloop) -> None: + async with async_client.benchmarks.with_streaming_response.start_run( + benchmark_id="benchmark_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + benchmark = await response.parse() + assert_matches_type(BenchmarkRunView, benchmark, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_update_scenarios(self, async_client: AsyncRunloop) -> None: + benchmark = await async_client.benchmarks.update_scenarios( + id="id", + ) + assert_matches_type(BenchmarkView, benchmark, path=["response"]) + + @parametrize + async def test_method_update_scenarios_with_all_params(self, async_client: AsyncRunloop) -> None: + benchmark = await async_client.benchmarks.update_scenarios( + id="id", + scenarios_to_add=["string"], + scenarios_to_remove=["string"], + ) + assert_matches_type(BenchmarkView, benchmark, path=["response"]) + + @parametrize + async def test_raw_response_update_scenarios(self, async_client: AsyncRunloop) -> None: + response = await async_client.benchmarks.with_raw_response.update_scenarios( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benchmark = await response.parse() + assert_matches_type(BenchmarkView, benchmark, path=["response"]) + + @parametrize + async def test_streaming_response_update_scenarios(self, async_client: AsyncRunloop) -> None: + async with async_client.benchmarks.with_streaming_response.update_scenarios( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + benchmark = await response.parse() + assert_matches_type(BenchmarkView, benchmark, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_update_scenarios(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.benchmarks.with_raw_response.update_scenarios( + id="", + ) diff --git a/tests/api_resources/test_blueprints.py b/tests/api_resources/test_blueprints.py new file mode 100644 index 000000000..383e70380 --- /dev/null +++ b/tests/api_resources/test_blueprints.py @@ -0,0 +1,905 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from runloop_api_client import Runloop, AsyncRunloop +from runloop_api_client.types import ( + BlueprintView, + BlueprintPreviewView, + BlueprintBuildLogsListView, +) +from runloop_api_client.pagination import SyncBlueprintsCursorIDPage, AsyncBlueprintsCursorIDPage + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestBlueprints: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Runloop) -> None: + blueprint = client.blueprints.create( + name="name", + ) + assert_matches_type(BlueprintView, blueprint, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: Runloop) -> None: + blueprint = client.blueprints.create( + name="name", + base_blueprint_id="base_blueprint_id", + base_blueprint_name="base_blueprint_name", + build_args={"foo": "string"}, + build_context={ + "object_id": "object_id", + "type": "object", + }, + code_mounts=[ + { + "repo_name": "repo_name", + "repo_owner": "repo_owner", + "token": "token", + "install_command": "install_command", + } + ], + dockerfile="dockerfile", + file_mounts={"foo": "string"}, + launch_parameters={ + "after_idle": { + "idle_time_seconds": 0, + "on_idle": "shutdown", + }, + "architecture": "x86_64", + "available_ports": [0], + "custom_cpu_cores": 0, + "custom_disk_size": 0, + "custom_gb_memory": 0, + "keep_alive_time_seconds": 0, + "launch_commands": ["string"], + "network_policy_id": "network_policy_id", + "required_services": ["string"], + "resource_size_request": "X_SMALL", + "user_parameters": { + "uid": 0, + "username": "username", + }, + }, + metadata={"foo": "string"}, + network_policy_id="network_policy_id", + secrets={"foo": "string"}, + services=[ + { + "image": "image", + "name": "name", + "credentials": { + "password": "password", + "username": "username", + }, + "env": {"foo": "string"}, + "options": "options", + "port_mappings": ["string"], + } + ], + system_setup_commands=["string"], + ) + assert_matches_type(BlueprintView, blueprint, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Runloop) -> None: + response = client.blueprints.with_raw_response.create( + name="name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + blueprint = response.parse() + assert_matches_type(BlueprintView, blueprint, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Runloop) -> None: + with client.blueprints.with_streaming_response.create( + name="name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + blueprint = response.parse() + assert_matches_type(BlueprintView, blueprint, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_retrieve(self, client: Runloop) -> None: + blueprint = client.blueprints.retrieve( + "id", + ) + assert_matches_type(BlueprintView, blueprint, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Runloop) -> None: + response = client.blueprints.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + blueprint = response.parse() + assert_matches_type(BlueprintView, blueprint, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Runloop) -> None: + with client.blueprints.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + blueprint = response.parse() + assert_matches_type(BlueprintView, blueprint, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.blueprints.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_list(self, client: Runloop) -> None: + blueprint = client.blueprints.list() + assert_matches_type(SyncBlueprintsCursorIDPage[BlueprintView], blueprint, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Runloop) -> None: + blueprint = client.blueprints.list( + limit=0, + name="name", + starting_after="starting_after", + status="status", + ) + assert_matches_type(SyncBlueprintsCursorIDPage[BlueprintView], blueprint, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Runloop) -> None: + response = client.blueprints.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + blueprint = response.parse() + assert_matches_type(SyncBlueprintsCursorIDPage[BlueprintView], blueprint, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Runloop) -> None: + with client.blueprints.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + blueprint = response.parse() + assert_matches_type(SyncBlueprintsCursorIDPage[BlueprintView], blueprint, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_delete(self, client: Runloop) -> None: + blueprint = client.blueprints.delete( + "id", + ) + assert_matches_type(object, blueprint, path=["response"]) + + @parametrize + def test_raw_response_delete(self, client: Runloop) -> None: + response = client.blueprints.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + blueprint = response.parse() + assert_matches_type(object, blueprint, path=["response"]) + + @parametrize + def test_streaming_response_delete(self, client: Runloop) -> None: + with client.blueprints.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + blueprint = response.parse() + assert_matches_type(object, blueprint, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_delete(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.blueprints.with_raw_response.delete( + "", + ) + + @parametrize + def test_method_create_from_inspection(self, client: Runloop) -> None: + blueprint = client.blueprints.create_from_inspection( + inspection_source={"inspection_id": "inspection_id"}, + name="name", + ) + assert_matches_type(BlueprintView, blueprint, path=["response"]) + + @parametrize + def test_method_create_from_inspection_with_all_params(self, client: Runloop) -> None: + blueprint = client.blueprints.create_from_inspection( + inspection_source={ + "inspection_id": "inspection_id", + "github_auth_token": "github_auth_token", + }, + name="name", + file_mounts={"foo": "string"}, + launch_parameters={ + "after_idle": { + "idle_time_seconds": 0, + "on_idle": "shutdown", + }, + "architecture": "x86_64", + "available_ports": [0], + "custom_cpu_cores": 0, + "custom_disk_size": 0, + "custom_gb_memory": 0, + "keep_alive_time_seconds": 0, + "launch_commands": ["string"], + "network_policy_id": "network_policy_id", + "required_services": ["string"], + "resource_size_request": "X_SMALL", + "user_parameters": { + "uid": 0, + "username": "username", + }, + }, + metadata={"foo": "string"}, + network_policy_id="network_policy_id", + secrets={"foo": "string"}, + system_setup_commands=["string"], + ) + assert_matches_type(BlueprintView, blueprint, path=["response"]) + + @parametrize + def test_raw_response_create_from_inspection(self, client: Runloop) -> None: + response = client.blueprints.with_raw_response.create_from_inspection( + inspection_source={"inspection_id": "inspection_id"}, + name="name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + blueprint = response.parse() + assert_matches_type(BlueprintView, blueprint, path=["response"]) + + @parametrize + def test_streaming_response_create_from_inspection(self, client: Runloop) -> None: + with client.blueprints.with_streaming_response.create_from_inspection( + inspection_source={"inspection_id": "inspection_id"}, + name="name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + blueprint = response.parse() + assert_matches_type(BlueprintView, blueprint, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_list_public(self, client: Runloop) -> None: + blueprint = client.blueprints.list_public() + assert_matches_type(SyncBlueprintsCursorIDPage[BlueprintView], blueprint, path=["response"]) + + @parametrize + def test_method_list_public_with_all_params(self, client: Runloop) -> None: + blueprint = client.blueprints.list_public( + limit=0, + name="name", + starting_after="starting_after", + status="status", + ) + assert_matches_type(SyncBlueprintsCursorIDPage[BlueprintView], blueprint, path=["response"]) + + @parametrize + def test_raw_response_list_public(self, client: Runloop) -> None: + response = client.blueprints.with_raw_response.list_public() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + blueprint = response.parse() + assert_matches_type(SyncBlueprintsCursorIDPage[BlueprintView], blueprint, path=["response"]) + + @parametrize + def test_streaming_response_list_public(self, client: Runloop) -> None: + with client.blueprints.with_streaming_response.list_public() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + blueprint = response.parse() + assert_matches_type(SyncBlueprintsCursorIDPage[BlueprintView], blueprint, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_logs(self, client: Runloop) -> None: + blueprint = client.blueprints.logs( + "id", + ) + assert_matches_type(BlueprintBuildLogsListView, blueprint, path=["response"]) + + @parametrize + def test_raw_response_logs(self, client: Runloop) -> None: + response = client.blueprints.with_raw_response.logs( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + blueprint = response.parse() + assert_matches_type(BlueprintBuildLogsListView, blueprint, path=["response"]) + + @parametrize + def test_streaming_response_logs(self, client: Runloop) -> None: + with client.blueprints.with_streaming_response.logs( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + blueprint = response.parse() + assert_matches_type(BlueprintBuildLogsListView, blueprint, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_logs(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.blueprints.with_raw_response.logs( + "", + ) + + @parametrize + def test_method_preview(self, client: Runloop) -> None: + blueprint = client.blueprints.preview( + name="name", + ) + assert_matches_type(BlueprintPreviewView, blueprint, path=["response"]) + + @parametrize + def test_method_preview_with_all_params(self, client: Runloop) -> None: + blueprint = client.blueprints.preview( + name="name", + base_blueprint_id="base_blueprint_id", + base_blueprint_name="base_blueprint_name", + build_args={"foo": "string"}, + build_context={ + "object_id": "object_id", + "type": "object", + }, + code_mounts=[ + { + "repo_name": "repo_name", + "repo_owner": "repo_owner", + "token": "token", + "install_command": "install_command", + } + ], + dockerfile="dockerfile", + file_mounts={"foo": "string"}, + launch_parameters={ + "after_idle": { + "idle_time_seconds": 0, + "on_idle": "shutdown", + }, + "architecture": "x86_64", + "available_ports": [0], + "custom_cpu_cores": 0, + "custom_disk_size": 0, + "custom_gb_memory": 0, + "keep_alive_time_seconds": 0, + "launch_commands": ["string"], + "network_policy_id": "network_policy_id", + "required_services": ["string"], + "resource_size_request": "X_SMALL", + "user_parameters": { + "uid": 0, + "username": "username", + }, + }, + metadata={"foo": "string"}, + network_policy_id="network_policy_id", + secrets={"foo": "string"}, + services=[ + { + "image": "image", + "name": "name", + "credentials": { + "password": "password", + "username": "username", + }, + "env": {"foo": "string"}, + "options": "options", + "port_mappings": ["string"], + } + ], + system_setup_commands=["string"], + ) + assert_matches_type(BlueprintPreviewView, blueprint, path=["response"]) + + @parametrize + def test_raw_response_preview(self, client: Runloop) -> None: + response = client.blueprints.with_raw_response.preview( + name="name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + blueprint = response.parse() + assert_matches_type(BlueprintPreviewView, blueprint, path=["response"]) + + @parametrize + def test_streaming_response_preview(self, client: Runloop) -> None: + with client.blueprints.with_streaming_response.preview( + name="name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + blueprint = response.parse() + assert_matches_type(BlueprintPreviewView, blueprint, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncBlueprints: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_create(self, async_client: AsyncRunloop) -> None: + blueprint = await async_client.blueprints.create( + name="name", + ) + assert_matches_type(BlueprintView, blueprint, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncRunloop) -> None: + blueprint = await async_client.blueprints.create( + name="name", + base_blueprint_id="base_blueprint_id", + base_blueprint_name="base_blueprint_name", + build_args={"foo": "string"}, + build_context={ + "object_id": "object_id", + "type": "object", + }, + code_mounts=[ + { + "repo_name": "repo_name", + "repo_owner": "repo_owner", + "token": "token", + "install_command": "install_command", + } + ], + dockerfile="dockerfile", + file_mounts={"foo": "string"}, + launch_parameters={ + "after_idle": { + "idle_time_seconds": 0, + "on_idle": "shutdown", + }, + "architecture": "x86_64", + "available_ports": [0], + "custom_cpu_cores": 0, + "custom_disk_size": 0, + "custom_gb_memory": 0, + "keep_alive_time_seconds": 0, + "launch_commands": ["string"], + "network_policy_id": "network_policy_id", + "required_services": ["string"], + "resource_size_request": "X_SMALL", + "user_parameters": { + "uid": 0, + "username": "username", + }, + }, + metadata={"foo": "string"}, + network_policy_id="network_policy_id", + secrets={"foo": "string"}, + services=[ + { + "image": "image", + "name": "name", + "credentials": { + "password": "password", + "username": "username", + }, + "env": {"foo": "string"}, + "options": "options", + "port_mappings": ["string"], + } + ], + system_setup_commands=["string"], + ) + assert_matches_type(BlueprintView, blueprint, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncRunloop) -> None: + response = await async_client.blueprints.with_raw_response.create( + name="name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + blueprint = await response.parse() + assert_matches_type(BlueprintView, blueprint, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncRunloop) -> None: + async with async_client.blueprints.with_streaming_response.create( + name="name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + blueprint = await response.parse() + assert_matches_type(BlueprintView, blueprint, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_retrieve(self, async_client: AsyncRunloop) -> None: + blueprint = await async_client.blueprints.retrieve( + "id", + ) + assert_matches_type(BlueprintView, blueprint, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncRunloop) -> None: + response = await async_client.blueprints.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + blueprint = await response.parse() + assert_matches_type(BlueprintView, blueprint, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncRunloop) -> None: + async with async_client.blueprints.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + blueprint = await response.parse() + assert_matches_type(BlueprintView, blueprint, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.blueprints.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncRunloop) -> None: + blueprint = await async_client.blueprints.list() + assert_matches_type(AsyncBlueprintsCursorIDPage[BlueprintView], blueprint, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncRunloop) -> None: + blueprint = await async_client.blueprints.list( + limit=0, + name="name", + starting_after="starting_after", + status="status", + ) + assert_matches_type(AsyncBlueprintsCursorIDPage[BlueprintView], blueprint, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncRunloop) -> None: + response = await async_client.blueprints.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + blueprint = await response.parse() + assert_matches_type(AsyncBlueprintsCursorIDPage[BlueprintView], blueprint, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncRunloop) -> None: + async with async_client.blueprints.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + blueprint = await response.parse() + assert_matches_type(AsyncBlueprintsCursorIDPage[BlueprintView], blueprint, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_delete(self, async_client: AsyncRunloop) -> None: + blueprint = await async_client.blueprints.delete( + "id", + ) + assert_matches_type(object, blueprint, path=["response"]) + + @parametrize + async def test_raw_response_delete(self, async_client: AsyncRunloop) -> None: + response = await async_client.blueprints.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + blueprint = await response.parse() + assert_matches_type(object, blueprint, path=["response"]) + + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncRunloop) -> None: + async with async_client.blueprints.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + blueprint = await response.parse() + assert_matches_type(object, blueprint, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_delete(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.blueprints.with_raw_response.delete( + "", + ) + + @parametrize + async def test_method_create_from_inspection(self, async_client: AsyncRunloop) -> None: + blueprint = await async_client.blueprints.create_from_inspection( + inspection_source={"inspection_id": "inspection_id"}, + name="name", + ) + assert_matches_type(BlueprintView, blueprint, path=["response"]) + + @parametrize + async def test_method_create_from_inspection_with_all_params(self, async_client: AsyncRunloop) -> None: + blueprint = await async_client.blueprints.create_from_inspection( + inspection_source={ + "inspection_id": "inspection_id", + "github_auth_token": "github_auth_token", + }, + name="name", + file_mounts={"foo": "string"}, + launch_parameters={ + "after_idle": { + "idle_time_seconds": 0, + "on_idle": "shutdown", + }, + "architecture": "x86_64", + "available_ports": [0], + "custom_cpu_cores": 0, + "custom_disk_size": 0, + "custom_gb_memory": 0, + "keep_alive_time_seconds": 0, + "launch_commands": ["string"], + "network_policy_id": "network_policy_id", + "required_services": ["string"], + "resource_size_request": "X_SMALL", + "user_parameters": { + "uid": 0, + "username": "username", + }, + }, + metadata={"foo": "string"}, + network_policy_id="network_policy_id", + secrets={"foo": "string"}, + system_setup_commands=["string"], + ) + assert_matches_type(BlueprintView, blueprint, path=["response"]) + + @parametrize + async def test_raw_response_create_from_inspection(self, async_client: AsyncRunloop) -> None: + response = await async_client.blueprints.with_raw_response.create_from_inspection( + inspection_source={"inspection_id": "inspection_id"}, + name="name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + blueprint = await response.parse() + assert_matches_type(BlueprintView, blueprint, path=["response"]) + + @parametrize + async def test_streaming_response_create_from_inspection(self, async_client: AsyncRunloop) -> None: + async with async_client.blueprints.with_streaming_response.create_from_inspection( + inspection_source={"inspection_id": "inspection_id"}, + name="name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + blueprint = await response.parse() + assert_matches_type(BlueprintView, blueprint, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_list_public(self, async_client: AsyncRunloop) -> None: + blueprint = await async_client.blueprints.list_public() + assert_matches_type(AsyncBlueprintsCursorIDPage[BlueprintView], blueprint, path=["response"]) + + @parametrize + async def test_method_list_public_with_all_params(self, async_client: AsyncRunloop) -> None: + blueprint = await async_client.blueprints.list_public( + limit=0, + name="name", + starting_after="starting_after", + status="status", + ) + assert_matches_type(AsyncBlueprintsCursorIDPage[BlueprintView], blueprint, path=["response"]) + + @parametrize + async def test_raw_response_list_public(self, async_client: AsyncRunloop) -> None: + response = await async_client.blueprints.with_raw_response.list_public() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + blueprint = await response.parse() + assert_matches_type(AsyncBlueprintsCursorIDPage[BlueprintView], blueprint, path=["response"]) + + @parametrize + async def test_streaming_response_list_public(self, async_client: AsyncRunloop) -> None: + async with async_client.blueprints.with_streaming_response.list_public() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + blueprint = await response.parse() + assert_matches_type(AsyncBlueprintsCursorIDPage[BlueprintView], blueprint, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_logs(self, async_client: AsyncRunloop) -> None: + blueprint = await async_client.blueprints.logs( + "id", + ) + assert_matches_type(BlueprintBuildLogsListView, blueprint, path=["response"]) + + @parametrize + async def test_raw_response_logs(self, async_client: AsyncRunloop) -> None: + response = await async_client.blueprints.with_raw_response.logs( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + blueprint = await response.parse() + assert_matches_type(BlueprintBuildLogsListView, blueprint, path=["response"]) + + @parametrize + async def test_streaming_response_logs(self, async_client: AsyncRunloop) -> None: + async with async_client.blueprints.with_streaming_response.logs( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + blueprint = await response.parse() + assert_matches_type(BlueprintBuildLogsListView, blueprint, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_logs(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.blueprints.with_raw_response.logs( + "", + ) + + @parametrize + async def test_method_preview(self, async_client: AsyncRunloop) -> None: + blueprint = await async_client.blueprints.preview( + name="name", + ) + assert_matches_type(BlueprintPreviewView, blueprint, path=["response"]) + + @parametrize + async def test_method_preview_with_all_params(self, async_client: AsyncRunloop) -> None: + blueprint = await async_client.blueprints.preview( + name="name", + base_blueprint_id="base_blueprint_id", + base_blueprint_name="base_blueprint_name", + build_args={"foo": "string"}, + build_context={ + "object_id": "object_id", + "type": "object", + }, + code_mounts=[ + { + "repo_name": "repo_name", + "repo_owner": "repo_owner", + "token": "token", + "install_command": "install_command", + } + ], + dockerfile="dockerfile", + file_mounts={"foo": "string"}, + launch_parameters={ + "after_idle": { + "idle_time_seconds": 0, + "on_idle": "shutdown", + }, + "architecture": "x86_64", + "available_ports": [0], + "custom_cpu_cores": 0, + "custom_disk_size": 0, + "custom_gb_memory": 0, + "keep_alive_time_seconds": 0, + "launch_commands": ["string"], + "network_policy_id": "network_policy_id", + "required_services": ["string"], + "resource_size_request": "X_SMALL", + "user_parameters": { + "uid": 0, + "username": "username", + }, + }, + metadata={"foo": "string"}, + network_policy_id="network_policy_id", + secrets={"foo": "string"}, + services=[ + { + "image": "image", + "name": "name", + "credentials": { + "password": "password", + "username": "username", + }, + "env": {"foo": "string"}, + "options": "options", + "port_mappings": ["string"], + } + ], + system_setup_commands=["string"], + ) + assert_matches_type(BlueprintPreviewView, blueprint, path=["response"]) + + @parametrize + async def test_raw_response_preview(self, async_client: AsyncRunloop) -> None: + response = await async_client.blueprints.with_raw_response.preview( + name="name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + blueprint = await response.parse() + assert_matches_type(BlueprintPreviewView, blueprint, path=["response"]) + + @parametrize + async def test_streaming_response_preview(self, async_client: AsyncRunloop) -> None: + async with async_client.blueprints.with_streaming_response.preview( + name="name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + blueprint = await response.parse() + assert_matches_type(BlueprintPreviewView, blueprint, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_devboxes.py b/tests/api_resources/test_devboxes.py new file mode 100644 index 000000000..f0c41e9a7 --- /dev/null +++ b/tests/api_resources/test_devboxes.py @@ -0,0 +1,2427 @@ +# 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 tests.utils import assert_matches_type +from runloop_api_client import Runloop, AsyncRunloop +from runloop_api_client.types import ( + DevboxView, + TunnelView, + DevboxTunnelView, + DevboxSnapshotView, + DevboxResourceUsageView, + DevboxExecutionDetailView, + DevboxCreateSSHKeyResponse, + DevboxAsyncExecutionDetailView, +) +from runloop_api_client._response import ( + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, +) +from runloop_api_client.pagination import ( + SyncDevboxesCursorIDPage, + AsyncDevboxesCursorIDPage, + SyncDiskSnapshotsCursorIDPage, + AsyncDiskSnapshotsCursorIDPage, +) + +# pyright: reportDeprecated=false + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestDevboxes: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Runloop) -> None: + devbox = client.devboxes.create() + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: Runloop) -> None: + devbox = client.devboxes.create( + blueprint_id="blueprint_id", + blueprint_name="blueprint_name", + code_mounts=[ + { + "repo_name": "repo_name", + "repo_owner": "repo_owner", + "token": "token", + "install_command": "install_command", + } + ], + entrypoint="entrypoint", + environment_variables={"foo": "string"}, + file_mounts={"foo": "string"}, + gateways={ + "foo": { + "gateway": "gateway", + "secret": "secret", + } + }, + launch_parameters={ + "after_idle": { + "idle_time_seconds": 0, + "on_idle": "shutdown", + }, + "architecture": "x86_64", + "available_ports": [0], + "custom_cpu_cores": 0, + "custom_disk_size": 0, + "custom_gb_memory": 0, + "keep_alive_time_seconds": 0, + "launch_commands": ["string"], + "network_policy_id": "network_policy_id", + "required_services": ["string"], + "resource_size_request": "X_SMALL", + "user_parameters": { + "uid": 0, + "username": "username", + }, + }, + mcp={ + "foo": { + "mcp_config": "mcp_config", + "secret": "secret", + } + }, + metadata={"foo": "string"}, + mounts=[ + { + "object_id": "object_id", + "object_path": "object_path", + "type": "object_mount", + } + ], + name="name", + repo_connection_id="repo_connection_id", + secrets={"foo": "string"}, + snapshot_id="snapshot_id", + tunnel={ + "auth_mode": "open", + "http_keep_alive": True, + }, + ) + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Runloop) -> None: + response = client.devboxes.with_raw_response.create() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = response.parse() + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Runloop) -> None: + with client.devboxes.with_streaming_response.create() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = response.parse() + assert_matches_type(DevboxView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_retrieve(self, client: Runloop) -> None: + devbox = client.devboxes.retrieve( + "id", + ) + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Runloop) -> None: + response = client.devboxes.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = response.parse() + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Runloop) -> None: + with client.devboxes.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = response.parse() + assert_matches_type(DevboxView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_update(self, client: Runloop) -> None: + devbox = client.devboxes.update( + id="id", + ) + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + def test_method_update_with_all_params(self, client: Runloop) -> None: + devbox = client.devboxes.update( + id="id", + metadata={"foo": "string"}, + name="name", + ) + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + def test_raw_response_update(self, client: Runloop) -> None: + response = client.devboxes.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = response.parse() + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + def test_streaming_response_update(self, client: Runloop) -> None: + with client.devboxes.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = response.parse() + assert_matches_type(DevboxView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_update(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.with_raw_response.update( + id="", + ) + + @parametrize + def test_method_list(self, client: Runloop) -> None: + devbox = client.devboxes.list() + assert_matches_type(SyncDevboxesCursorIDPage[DevboxView], devbox, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Runloop) -> None: + devbox = client.devboxes.list( + limit=0, + starting_after="starting_after", + status="provisioning", + ) + assert_matches_type(SyncDevboxesCursorIDPage[DevboxView], devbox, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Runloop) -> None: + response = client.devboxes.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = response.parse() + assert_matches_type(SyncDevboxesCursorIDPage[DevboxView], devbox, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Runloop) -> None: + with client.devboxes.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = response.parse() + assert_matches_type(SyncDevboxesCursorIDPage[DevboxView], devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_create_ssh_key(self, client: Runloop) -> None: + devbox = client.devboxes.create_ssh_key( + "id", + ) + assert_matches_type(DevboxCreateSSHKeyResponse, devbox, path=["response"]) + + @parametrize + def test_raw_response_create_ssh_key(self, client: Runloop) -> None: + response = client.devboxes.with_raw_response.create_ssh_key( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = response.parse() + assert_matches_type(DevboxCreateSSHKeyResponse, devbox, path=["response"]) + + @parametrize + def test_streaming_response_create_ssh_key(self, client: Runloop) -> None: + with client.devboxes.with_streaming_response.create_ssh_key( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = response.parse() + assert_matches_type(DevboxCreateSSHKeyResponse, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_create_ssh_key(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.with_raw_response.create_ssh_key( + "", + ) + + @parametrize + def test_method_create_tunnel(self, client: Runloop) -> None: + with pytest.warns(DeprecationWarning): + devbox = client.devboxes.create_tunnel( + id="id", + port=0, + ) + + assert_matches_type(DevboxTunnelView, devbox, path=["response"]) + + @parametrize + def test_raw_response_create_tunnel(self, client: Runloop) -> None: + with pytest.warns(DeprecationWarning): + response = client.devboxes.with_raw_response.create_tunnel( + id="id", + port=0, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = response.parse() + assert_matches_type(DevboxTunnelView, devbox, path=["response"]) + + @parametrize + def test_streaming_response_create_tunnel(self, client: Runloop) -> None: + with pytest.warns(DeprecationWarning): + with client.devboxes.with_streaming_response.create_tunnel( + id="id", + port=0, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = response.parse() + assert_matches_type(DevboxTunnelView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_create_tunnel(self, client: Runloop) -> None: + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.with_raw_response.create_tunnel( + id="", + port=0, + ) + + @parametrize + def test_method_delete_disk_snapshot(self, client: Runloop) -> None: + devbox = client.devboxes.delete_disk_snapshot( + "id", + ) + assert_matches_type(object, devbox, path=["response"]) + + @parametrize + def test_raw_response_delete_disk_snapshot(self, client: Runloop) -> None: + response = client.devboxes.with_raw_response.delete_disk_snapshot( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = response.parse() + assert_matches_type(object, devbox, path=["response"]) + + @parametrize + def test_streaming_response_delete_disk_snapshot(self, client: Runloop) -> None: + with client.devboxes.with_streaming_response.delete_disk_snapshot( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = response.parse() + assert_matches_type(object, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_delete_disk_snapshot(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.with_raw_response.delete_disk_snapshot( + "", + ) + + @pytest.mark.skip(reason="prism can't support octet") + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_method_download_file(self, client: Runloop, respx_mock: MockRouter) -> None: + respx_mock.post("/v1/devboxes/id/download_file").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + devbox = client.devboxes.download_file( + id="id", + path="path", + ) + assert devbox.is_closed + assert devbox.json() == {"foo": "bar"} + assert cast(Any, devbox.is_closed) is True + assert isinstance(devbox, BinaryAPIResponse) + + @pytest.mark.skip(reason="prism can't support octet") + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_raw_response_download_file(self, client: Runloop, respx_mock: MockRouter) -> None: + respx_mock.post("/v1/devboxes/id/download_file").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + devbox = client.devboxes.with_raw_response.download_file( + id="id", + path="path", + ) + + assert devbox.is_closed is True + assert devbox.http_request.headers.get("X-Stainless-Lang") == "python" + assert devbox.json() == {"foo": "bar"} + assert isinstance(devbox, BinaryAPIResponse) + + @pytest.mark.skip(reason="prism can't support octet") + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_streaming_response_download_file(self, client: Runloop, respx_mock: MockRouter) -> None: + respx_mock.post("/v1/devboxes/id/download_file").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + with client.devboxes.with_streaming_response.download_file( + id="id", + path="path", + ) as devbox: + assert not devbox.is_closed + assert devbox.http_request.headers.get("X-Stainless-Lang") == "python" + + assert devbox.json() == {"foo": "bar"} + assert cast(Any, devbox.is_closed) is True + assert isinstance(devbox, StreamedBinaryAPIResponse) + + assert cast(Any, devbox.is_closed) is True + + @pytest.mark.skip(reason="prism can't support octet") + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_path_params_download_file(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.with_raw_response.download_file( + id="", + path="path", + ) + + @parametrize + def test_method_enable_tunnel(self, client: Runloop) -> None: + devbox = client.devboxes.enable_tunnel( + id="id", + ) + assert_matches_type(TunnelView, devbox, path=["response"]) + + @parametrize + def test_method_enable_tunnel_with_all_params(self, client: Runloop) -> None: + devbox = client.devboxes.enable_tunnel( + id="id", + auth_mode="open", + http_keep_alive=True, + ) + assert_matches_type(TunnelView, devbox, path=["response"]) + + @parametrize + def test_raw_response_enable_tunnel(self, client: Runloop) -> None: + response = client.devboxes.with_raw_response.enable_tunnel( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = response.parse() + assert_matches_type(TunnelView, devbox, path=["response"]) + + @parametrize + def test_streaming_response_enable_tunnel(self, client: Runloop) -> None: + with client.devboxes.with_streaming_response.enable_tunnel( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = response.parse() + assert_matches_type(TunnelView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_enable_tunnel(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.with_raw_response.enable_tunnel( + id="", + ) + + @parametrize + def test_method_execute(self, client: Runloop) -> None: + devbox = client.devboxes.execute( + id="id", + command="command", + command_id="command_id", + ) + assert_matches_type(DevboxAsyncExecutionDetailView, devbox, path=["response"]) + + @parametrize + def test_method_execute_with_all_params(self, client: Runloop) -> None: + devbox = client.devboxes.execute( + id="id", + command="command", + command_id="command_id", + last_n="last_n", + optimistic_timeout=0, + shell_name="shell_name", + ) + assert_matches_type(DevboxAsyncExecutionDetailView, devbox, path=["response"]) + + @parametrize + def test_raw_response_execute(self, client: Runloop) -> None: + response = client.devboxes.with_raw_response.execute( + id="id", + command="command", + command_id="command_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = response.parse() + assert_matches_type(DevboxAsyncExecutionDetailView, devbox, path=["response"]) + + @parametrize + def test_streaming_response_execute(self, client: Runloop) -> None: + with client.devboxes.with_streaming_response.execute( + id="id", + command="command", + command_id="command_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = response.parse() + assert_matches_type(DevboxAsyncExecutionDetailView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_execute(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.with_raw_response.execute( + id="", + command="command", + command_id="command_id", + ) + + @parametrize + def test_method_execute_async(self, client: Runloop) -> None: + devbox = client.devboxes.execute_async( + id="id", + command="command", + ) + assert_matches_type(DevboxAsyncExecutionDetailView, devbox, path=["response"]) + + @parametrize + def test_method_execute_async_with_all_params(self, client: Runloop) -> None: + devbox = client.devboxes.execute_async( + id="id", + command="command", + attach_stdin=True, + shell_name="shell_name", + ) + assert_matches_type(DevboxAsyncExecutionDetailView, devbox, path=["response"]) + + @parametrize + def test_raw_response_execute_async(self, client: Runloop) -> None: + response = client.devboxes.with_raw_response.execute_async( + id="id", + command="command", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = response.parse() + assert_matches_type(DevboxAsyncExecutionDetailView, devbox, path=["response"]) + + @parametrize + def test_streaming_response_execute_async(self, client: Runloop) -> None: + with client.devboxes.with_streaming_response.execute_async( + id="id", + command="command", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = response.parse() + assert_matches_type(DevboxAsyncExecutionDetailView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_execute_async(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.with_raw_response.execute_async( + id="", + command="command", + ) + + @parametrize + def test_method_execute_sync(self, client: Runloop) -> None: + with pytest.warns(DeprecationWarning): + devbox = client.devboxes.execute_sync( + id="id", + command="command", + ) + + assert_matches_type(DevboxExecutionDetailView, devbox, path=["response"]) + + @parametrize + def test_method_execute_sync_with_all_params(self, client: Runloop) -> None: + with pytest.warns(DeprecationWarning): + devbox = client.devboxes.execute_sync( + id="id", + command="command", + attach_stdin=True, + shell_name="shell_name", + ) + + assert_matches_type(DevboxExecutionDetailView, devbox, path=["response"]) + + @parametrize + def test_raw_response_execute_sync(self, client: Runloop) -> None: + with pytest.warns(DeprecationWarning): + response = client.devboxes.with_raw_response.execute_sync( + id="id", + command="command", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = response.parse() + assert_matches_type(DevboxExecutionDetailView, devbox, path=["response"]) + + @parametrize + def test_streaming_response_execute_sync(self, client: Runloop) -> None: + with pytest.warns(DeprecationWarning): + with client.devboxes.with_streaming_response.execute_sync( + id="id", + command="command", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = response.parse() + assert_matches_type(DevboxExecutionDetailView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_execute_sync(self, client: Runloop) -> None: + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.with_raw_response.execute_sync( + id="", + command="command", + ) + + @parametrize + def test_method_keep_alive(self, client: Runloop) -> None: + devbox = client.devboxes.keep_alive( + "id", + ) + assert_matches_type(object, devbox, path=["response"]) + + @parametrize + def test_raw_response_keep_alive(self, client: Runloop) -> None: + response = client.devboxes.with_raw_response.keep_alive( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = response.parse() + assert_matches_type(object, devbox, path=["response"]) + + @parametrize + def test_streaming_response_keep_alive(self, client: Runloop) -> None: + with client.devboxes.with_streaming_response.keep_alive( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = response.parse() + assert_matches_type(object, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_keep_alive(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.with_raw_response.keep_alive( + "", + ) + + @parametrize + def test_method_list_disk_snapshots(self, client: Runloop) -> None: + devbox = client.devboxes.list_disk_snapshots() + assert_matches_type(SyncDiskSnapshotsCursorIDPage[DevboxSnapshotView], devbox, path=["response"]) + + @parametrize + def test_method_list_disk_snapshots_with_all_params(self, client: Runloop) -> None: + devbox = client.devboxes.list_disk_snapshots( + devbox_id="devbox_id", + limit=0, + metadata_key="metadata[key]", + metadata_key_in="metadata[key][in]", + source_blueprint_id="source_blueprint_id", + starting_after="starting_after", + ) + assert_matches_type(SyncDiskSnapshotsCursorIDPage[DevboxSnapshotView], devbox, path=["response"]) + + @parametrize + def test_raw_response_list_disk_snapshots(self, client: Runloop) -> None: + response = client.devboxes.with_raw_response.list_disk_snapshots() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = response.parse() + assert_matches_type(SyncDiskSnapshotsCursorIDPage[DevboxSnapshotView], devbox, path=["response"]) + + @parametrize + def test_streaming_response_list_disk_snapshots(self, client: Runloop) -> None: + with client.devboxes.with_streaming_response.list_disk_snapshots() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = response.parse() + assert_matches_type(SyncDiskSnapshotsCursorIDPage[DevboxSnapshotView], devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_read_file_contents(self, client: Runloop) -> None: + devbox = client.devboxes.read_file_contents( + id="id", + file_path="file_path", + ) + assert_matches_type(str, devbox, path=["response"]) + + @parametrize + def test_raw_response_read_file_contents(self, client: Runloop) -> None: + response = client.devboxes.with_raw_response.read_file_contents( + id="id", + file_path="file_path", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = response.parse() + assert_matches_type(str, devbox, path=["response"]) + + @parametrize + def test_streaming_response_read_file_contents(self, client: Runloop) -> None: + with client.devboxes.with_streaming_response.read_file_contents( + id="id", + file_path="file_path", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = response.parse() + assert_matches_type(str, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_read_file_contents(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.with_raw_response.read_file_contents( + id="", + file_path="file_path", + ) + + @parametrize + def test_method_remove_tunnel(self, client: Runloop) -> None: + with pytest.warns(DeprecationWarning): + devbox = client.devboxes.remove_tunnel( + id="id", + port=0, + ) + + assert_matches_type(object, devbox, path=["response"]) + + @parametrize + def test_raw_response_remove_tunnel(self, client: Runloop) -> None: + with pytest.warns(DeprecationWarning): + response = client.devboxes.with_raw_response.remove_tunnel( + id="id", + port=0, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = response.parse() + assert_matches_type(object, devbox, path=["response"]) + + @parametrize + def test_streaming_response_remove_tunnel(self, client: Runloop) -> None: + with pytest.warns(DeprecationWarning): + with client.devboxes.with_streaming_response.remove_tunnel( + id="id", + port=0, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = response.parse() + assert_matches_type(object, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_remove_tunnel(self, client: Runloop) -> None: + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.with_raw_response.remove_tunnel( + id="", + port=0, + ) + + @parametrize + def test_method_resume(self, client: Runloop) -> None: + devbox = client.devboxes.resume( + "id", + ) + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + def test_raw_response_resume(self, client: Runloop) -> None: + response = client.devboxes.with_raw_response.resume( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = response.parse() + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + def test_streaming_response_resume(self, client: Runloop) -> None: + with client.devboxes.with_streaming_response.resume( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = response.parse() + assert_matches_type(DevboxView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_resume(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.with_raw_response.resume( + "", + ) + + @parametrize + def test_method_retrieve_resource_usage(self, client: Runloop) -> None: + devbox = client.devboxes.retrieve_resource_usage( + "id", + ) + assert_matches_type(DevboxResourceUsageView, devbox, path=["response"]) + + @parametrize + def test_raw_response_retrieve_resource_usage(self, client: Runloop) -> None: + response = client.devboxes.with_raw_response.retrieve_resource_usage( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = response.parse() + assert_matches_type(DevboxResourceUsageView, devbox, path=["response"]) + + @parametrize + def test_streaming_response_retrieve_resource_usage(self, client: Runloop) -> None: + with client.devboxes.with_streaming_response.retrieve_resource_usage( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = response.parse() + assert_matches_type(DevboxResourceUsageView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve_resource_usage(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.with_raw_response.retrieve_resource_usage( + "", + ) + + @parametrize + def test_method_shutdown(self, client: Runloop) -> None: + devbox = client.devboxes.shutdown( + id="id", + ) + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + def test_method_shutdown_with_all_params(self, client: Runloop) -> None: + devbox = client.devboxes.shutdown( + id="id", + force="force", + ) + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + def test_raw_response_shutdown(self, client: Runloop) -> None: + response = client.devboxes.with_raw_response.shutdown( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = response.parse() + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + def test_streaming_response_shutdown(self, client: Runloop) -> None: + with client.devboxes.with_streaming_response.shutdown( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = response.parse() + assert_matches_type(DevboxView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_shutdown(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.with_raw_response.shutdown( + id="", + ) + + @parametrize + def test_method_snapshot_disk(self, client: Runloop) -> None: + devbox = client.devboxes.snapshot_disk( + id="id", + ) + assert_matches_type(DevboxSnapshotView, devbox, path=["response"]) + + @parametrize + def test_method_snapshot_disk_with_all_params(self, client: Runloop) -> None: + devbox = client.devboxes.snapshot_disk( + id="id", + commit_message="commit_message", + metadata={"foo": "string"}, + name="name", + ) + assert_matches_type(DevboxSnapshotView, devbox, path=["response"]) + + @parametrize + def test_raw_response_snapshot_disk(self, client: Runloop) -> None: + response = client.devboxes.with_raw_response.snapshot_disk( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = response.parse() + assert_matches_type(DevboxSnapshotView, devbox, path=["response"]) + + @parametrize + def test_streaming_response_snapshot_disk(self, client: Runloop) -> None: + with client.devboxes.with_streaming_response.snapshot_disk( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = response.parse() + assert_matches_type(DevboxSnapshotView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_snapshot_disk(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.with_raw_response.snapshot_disk( + id="", + ) + + @parametrize + def test_method_snapshot_disk_async(self, client: Runloop) -> None: + devbox = client.devboxes.snapshot_disk_async( + id="id", + ) + assert_matches_type(DevboxSnapshotView, devbox, path=["response"]) + + @parametrize + def test_method_snapshot_disk_async_with_all_params(self, client: Runloop) -> None: + devbox = client.devboxes.snapshot_disk_async( + id="id", + commit_message="commit_message", + metadata={"foo": "string"}, + name="name", + ) + assert_matches_type(DevboxSnapshotView, devbox, path=["response"]) + + @parametrize + def test_raw_response_snapshot_disk_async(self, client: Runloop) -> None: + response = client.devboxes.with_raw_response.snapshot_disk_async( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = response.parse() + assert_matches_type(DevboxSnapshotView, devbox, path=["response"]) + + @parametrize + def test_streaming_response_snapshot_disk_async(self, client: Runloop) -> None: + with client.devboxes.with_streaming_response.snapshot_disk_async( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = response.parse() + assert_matches_type(DevboxSnapshotView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_snapshot_disk_async(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.with_raw_response.snapshot_disk_async( + id="", + ) + + @parametrize + def test_method_suspend(self, client: Runloop) -> None: + devbox = client.devboxes.suspend( + "id", + ) + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + def test_raw_response_suspend(self, client: Runloop) -> None: + response = client.devboxes.with_raw_response.suspend( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = response.parse() + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + def test_streaming_response_suspend(self, client: Runloop) -> None: + with client.devboxes.with_streaming_response.suspend( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = response.parse() + assert_matches_type(DevboxView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_suspend(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.with_raw_response.suspend( + "", + ) + + @parametrize + def test_method_upload_file(self, client: Runloop) -> None: + devbox = client.devboxes.upload_file( + id="id", + path="path", + ) + assert_matches_type(object, devbox, path=["response"]) + + @parametrize + def test_method_upload_file_with_all_params(self, client: Runloop) -> None: + devbox = client.devboxes.upload_file( + id="id", + path="path", + file=b"Example data", + ) + assert_matches_type(object, devbox, path=["response"]) + + @parametrize + def test_raw_response_upload_file(self, client: Runloop) -> None: + response = client.devboxes.with_raw_response.upload_file( + id="id", + path="path", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = response.parse() + assert_matches_type(object, devbox, path=["response"]) + + @parametrize + def test_streaming_response_upload_file(self, client: Runloop) -> None: + with client.devboxes.with_streaming_response.upload_file( + id="id", + path="path", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = response.parse() + assert_matches_type(object, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_upload_file(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.with_raw_response.upload_file( + id="", + path="path", + ) + + @parametrize + def test_method_wait_for_command(self, client: Runloop) -> None: + devbox = client.devboxes.wait_for_command( + execution_id="execution_id", + devbox_id="devbox_id", + statuses=["queued"], + ) + assert_matches_type(DevboxAsyncExecutionDetailView, devbox, path=["response"]) + + @parametrize + def test_method_wait_for_command_with_all_params(self, client: Runloop) -> None: + devbox = client.devboxes.wait_for_command( + execution_id="execution_id", + devbox_id="devbox_id", + statuses=["queued"], + last_n="last_n", + timeout_seconds=0, + ) + assert_matches_type(DevboxAsyncExecutionDetailView, devbox, path=["response"]) + + @parametrize + def test_raw_response_wait_for_command(self, client: Runloop) -> None: + response = client.devboxes.with_raw_response.wait_for_command( + execution_id="execution_id", + devbox_id="devbox_id", + statuses=["queued"], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = response.parse() + assert_matches_type(DevboxAsyncExecutionDetailView, devbox, path=["response"]) + + @parametrize + def test_streaming_response_wait_for_command(self, client: Runloop) -> None: + with client.devboxes.with_streaming_response.wait_for_command( + execution_id="execution_id", + devbox_id="devbox_id", + statuses=["queued"], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = response.parse() + assert_matches_type(DevboxAsyncExecutionDetailView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_wait_for_command(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `devbox_id` but received ''"): + client.devboxes.with_raw_response.wait_for_command( + execution_id="execution_id", + devbox_id="", + statuses=["queued"], + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `execution_id` but received ''"): + client.devboxes.with_raw_response.wait_for_command( + execution_id="", + devbox_id="devbox_id", + statuses=["queued"], + ) + + @parametrize + def test_method_write_file_contents(self, client: Runloop) -> None: + devbox = client.devboxes.write_file_contents( + id="id", + contents="contents", + file_path="file_path", + ) + assert_matches_type(DevboxExecutionDetailView, devbox, path=["response"]) + + @parametrize + def test_raw_response_write_file_contents(self, client: Runloop) -> None: + response = client.devboxes.with_raw_response.write_file_contents( + id="id", + contents="contents", + file_path="file_path", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = response.parse() + assert_matches_type(DevboxExecutionDetailView, devbox, path=["response"]) + + @parametrize + def test_streaming_response_write_file_contents(self, client: Runloop) -> None: + with client.devboxes.with_streaming_response.write_file_contents( + id="id", + contents="contents", + file_path="file_path", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = response.parse() + assert_matches_type(DevboxExecutionDetailView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_write_file_contents(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.with_raw_response.write_file_contents( + id="", + contents="contents", + file_path="file_path", + ) + + +class TestAsyncDevboxes: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_create(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.create() + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.create( + blueprint_id="blueprint_id", + blueprint_name="blueprint_name", + code_mounts=[ + { + "repo_name": "repo_name", + "repo_owner": "repo_owner", + "token": "token", + "install_command": "install_command", + } + ], + entrypoint="entrypoint", + environment_variables={"foo": "string"}, + file_mounts={"foo": "string"}, + gateways={ + "foo": { + "gateway": "gateway", + "secret": "secret", + } + }, + launch_parameters={ + "after_idle": { + "idle_time_seconds": 0, + "on_idle": "shutdown", + }, + "architecture": "x86_64", + "available_ports": [0], + "custom_cpu_cores": 0, + "custom_disk_size": 0, + "custom_gb_memory": 0, + "keep_alive_time_seconds": 0, + "launch_commands": ["string"], + "network_policy_id": "network_policy_id", + "required_services": ["string"], + "resource_size_request": "X_SMALL", + "user_parameters": { + "uid": 0, + "username": "username", + }, + }, + mcp={ + "foo": { + "mcp_config": "mcp_config", + "secret": "secret", + } + }, + metadata={"foo": "string"}, + mounts=[ + { + "object_id": "object_id", + "object_path": "object_path", + "type": "object_mount", + } + ], + name="name", + repo_connection_id="repo_connection_id", + secrets={"foo": "string"}, + snapshot_id="snapshot_id", + tunnel={ + "auth_mode": "open", + "http_keep_alive": True, + }, + ) + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.with_raw_response.create() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = await response.parse() + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.with_streaming_response.create() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = await response.parse() + assert_matches_type(DevboxView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_retrieve(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.retrieve( + "id", + ) + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = await response.parse() + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = await response.parse() + assert_matches_type(DevboxView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_update(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.update( + id="id", + ) + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.update( + id="id", + metadata={"foo": "string"}, + name="name", + ) + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + async def test_raw_response_update(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = await response.parse() + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + async def test_streaming_response_update(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = await response.parse() + assert_matches_type(DevboxView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_update(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.with_raw_response.update( + id="", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.list() + assert_matches_type(AsyncDevboxesCursorIDPage[DevboxView], devbox, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.list( + limit=0, + starting_after="starting_after", + status="provisioning", + ) + assert_matches_type(AsyncDevboxesCursorIDPage[DevboxView], devbox, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = await response.parse() + assert_matches_type(AsyncDevboxesCursorIDPage[DevboxView], devbox, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = await response.parse() + assert_matches_type(AsyncDevboxesCursorIDPage[DevboxView], devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_create_ssh_key(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.create_ssh_key( + "id", + ) + assert_matches_type(DevboxCreateSSHKeyResponse, devbox, path=["response"]) + + @parametrize + async def test_raw_response_create_ssh_key(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.with_raw_response.create_ssh_key( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = await response.parse() + assert_matches_type(DevboxCreateSSHKeyResponse, devbox, path=["response"]) + + @parametrize + async def test_streaming_response_create_ssh_key(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.with_streaming_response.create_ssh_key( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = await response.parse() + assert_matches_type(DevboxCreateSSHKeyResponse, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_create_ssh_key(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.with_raw_response.create_ssh_key( + "", + ) + + @parametrize + async def test_method_create_tunnel(self, async_client: AsyncRunloop) -> None: + with pytest.warns(DeprecationWarning): + devbox = await async_client.devboxes.create_tunnel( + id="id", + port=0, + ) + + assert_matches_type(DevboxTunnelView, devbox, path=["response"]) + + @parametrize + async def test_raw_response_create_tunnel(self, async_client: AsyncRunloop) -> None: + with pytest.warns(DeprecationWarning): + response = await async_client.devboxes.with_raw_response.create_tunnel( + id="id", + port=0, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = await response.parse() + assert_matches_type(DevboxTunnelView, devbox, path=["response"]) + + @parametrize + async def test_streaming_response_create_tunnel(self, async_client: AsyncRunloop) -> None: + with pytest.warns(DeprecationWarning): + async with async_client.devboxes.with_streaming_response.create_tunnel( + id="id", + port=0, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = await response.parse() + assert_matches_type(DevboxTunnelView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_create_tunnel(self, async_client: AsyncRunloop) -> None: + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.with_raw_response.create_tunnel( + id="", + port=0, + ) + + @parametrize + async def test_method_delete_disk_snapshot(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.delete_disk_snapshot( + "id", + ) + assert_matches_type(object, devbox, path=["response"]) + + @parametrize + async def test_raw_response_delete_disk_snapshot(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.with_raw_response.delete_disk_snapshot( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = await response.parse() + assert_matches_type(object, devbox, path=["response"]) + + @parametrize + async def test_streaming_response_delete_disk_snapshot(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.with_streaming_response.delete_disk_snapshot( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = await response.parse() + assert_matches_type(object, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_delete_disk_snapshot(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.with_raw_response.delete_disk_snapshot( + "", + ) + + @pytest.mark.skip(reason="prism can't support octet") + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_method_download_file(self, async_client: AsyncRunloop, respx_mock: MockRouter) -> None: + respx_mock.post("/v1/devboxes/id/download_file").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + devbox = await async_client.devboxes.download_file( + id="id", + path="path", + ) + assert devbox.is_closed + assert await devbox.json() == {"foo": "bar"} + assert cast(Any, devbox.is_closed) is True + assert isinstance(devbox, AsyncBinaryAPIResponse) + + @pytest.mark.skip(reason="prism can't support octet") + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_raw_response_download_file(self, async_client: AsyncRunloop, respx_mock: MockRouter) -> None: + respx_mock.post("/v1/devboxes/id/download_file").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + devbox = await async_client.devboxes.with_raw_response.download_file( + id="id", + path="path", + ) + + assert devbox.is_closed is True + assert devbox.http_request.headers.get("X-Stainless-Lang") == "python" + assert await devbox.json() == {"foo": "bar"} + assert isinstance(devbox, AsyncBinaryAPIResponse) + + @pytest.mark.skip(reason="prism can't support octet") + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_streaming_response_download_file(self, async_client: AsyncRunloop, respx_mock: MockRouter) -> None: + respx_mock.post("/v1/devboxes/id/download_file").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + async with async_client.devboxes.with_streaming_response.download_file( + id="id", + path="path", + ) as devbox: + assert not devbox.is_closed + assert devbox.http_request.headers.get("X-Stainless-Lang") == "python" + + assert await devbox.json() == {"foo": "bar"} + assert cast(Any, devbox.is_closed) is True + assert isinstance(devbox, AsyncStreamedBinaryAPIResponse) + + assert cast(Any, devbox.is_closed) is True + + @pytest.mark.skip(reason="prism can't support octet") + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_path_params_download_file(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.with_raw_response.download_file( + id="", + path="path", + ) + + @parametrize + async def test_method_enable_tunnel(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.enable_tunnel( + id="id", + ) + assert_matches_type(TunnelView, devbox, path=["response"]) + + @parametrize + async def test_method_enable_tunnel_with_all_params(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.enable_tunnel( + id="id", + auth_mode="open", + http_keep_alive=True, + ) + assert_matches_type(TunnelView, devbox, path=["response"]) + + @parametrize + async def test_raw_response_enable_tunnel(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.with_raw_response.enable_tunnel( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = await response.parse() + assert_matches_type(TunnelView, devbox, path=["response"]) + + @parametrize + async def test_streaming_response_enable_tunnel(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.with_streaming_response.enable_tunnel( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = await response.parse() + assert_matches_type(TunnelView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_enable_tunnel(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.with_raw_response.enable_tunnel( + id="", + ) + + @parametrize + async def test_method_execute(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.execute( + id="id", + command="command", + command_id="command_id", + ) + assert_matches_type(DevboxAsyncExecutionDetailView, devbox, path=["response"]) + + @parametrize + async def test_method_execute_with_all_params(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.execute( + id="id", + command="command", + command_id="command_id", + last_n="last_n", + optimistic_timeout=0, + shell_name="shell_name", + ) + assert_matches_type(DevboxAsyncExecutionDetailView, devbox, path=["response"]) + + @parametrize + async def test_raw_response_execute(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.with_raw_response.execute( + id="id", + command="command", + command_id="command_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = await response.parse() + assert_matches_type(DevboxAsyncExecutionDetailView, devbox, path=["response"]) + + @parametrize + async def test_streaming_response_execute(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.with_streaming_response.execute( + id="id", + command="command", + command_id="command_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = await response.parse() + assert_matches_type(DevboxAsyncExecutionDetailView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_execute(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.with_raw_response.execute( + id="", + command="command", + command_id="command_id", + ) + + @parametrize + async def test_method_execute_async(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.execute_async( + id="id", + command="command", + ) + assert_matches_type(DevboxAsyncExecutionDetailView, devbox, path=["response"]) + + @parametrize + async def test_method_execute_async_with_all_params(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.execute_async( + id="id", + command="command", + attach_stdin=True, + shell_name="shell_name", + ) + assert_matches_type(DevboxAsyncExecutionDetailView, devbox, path=["response"]) + + @parametrize + async def test_raw_response_execute_async(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.with_raw_response.execute_async( + id="id", + command="command", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = await response.parse() + assert_matches_type(DevboxAsyncExecutionDetailView, devbox, path=["response"]) + + @parametrize + async def test_streaming_response_execute_async(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.with_streaming_response.execute_async( + id="id", + command="command", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = await response.parse() + assert_matches_type(DevboxAsyncExecutionDetailView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_execute_async(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.with_raw_response.execute_async( + id="", + command="command", + ) + + @parametrize + async def test_method_execute_sync(self, async_client: AsyncRunloop) -> None: + with pytest.warns(DeprecationWarning): + devbox = await async_client.devboxes.execute_sync( + id="id", + command="command", + ) + + assert_matches_type(DevboxExecutionDetailView, devbox, path=["response"]) + + @parametrize + async def test_method_execute_sync_with_all_params(self, async_client: AsyncRunloop) -> None: + with pytest.warns(DeprecationWarning): + devbox = await async_client.devboxes.execute_sync( + id="id", + command="command", + attach_stdin=True, + shell_name="shell_name", + ) + + assert_matches_type(DevboxExecutionDetailView, devbox, path=["response"]) + + @parametrize + async def test_raw_response_execute_sync(self, async_client: AsyncRunloop) -> None: + with pytest.warns(DeprecationWarning): + response = await async_client.devboxes.with_raw_response.execute_sync( + id="id", + command="command", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = await response.parse() + assert_matches_type(DevboxExecutionDetailView, devbox, path=["response"]) + + @parametrize + async def test_streaming_response_execute_sync(self, async_client: AsyncRunloop) -> None: + with pytest.warns(DeprecationWarning): + async with async_client.devboxes.with_streaming_response.execute_sync( + id="id", + command="command", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = await response.parse() + assert_matches_type(DevboxExecutionDetailView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_execute_sync(self, async_client: AsyncRunloop) -> None: + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.with_raw_response.execute_sync( + id="", + command="command", + ) + + @parametrize + async def test_method_keep_alive(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.keep_alive( + "id", + ) + assert_matches_type(object, devbox, path=["response"]) + + @parametrize + async def test_raw_response_keep_alive(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.with_raw_response.keep_alive( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = await response.parse() + assert_matches_type(object, devbox, path=["response"]) + + @parametrize + async def test_streaming_response_keep_alive(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.with_streaming_response.keep_alive( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = await response.parse() + assert_matches_type(object, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_keep_alive(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.with_raw_response.keep_alive( + "", + ) + + @parametrize + async def test_method_list_disk_snapshots(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.list_disk_snapshots() + assert_matches_type(AsyncDiskSnapshotsCursorIDPage[DevboxSnapshotView], devbox, path=["response"]) + + @parametrize + async def test_method_list_disk_snapshots_with_all_params(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.list_disk_snapshots( + devbox_id="devbox_id", + limit=0, + metadata_key="metadata[key]", + metadata_key_in="metadata[key][in]", + source_blueprint_id="source_blueprint_id", + starting_after="starting_after", + ) + assert_matches_type(AsyncDiskSnapshotsCursorIDPage[DevboxSnapshotView], devbox, path=["response"]) + + @parametrize + async def test_raw_response_list_disk_snapshots(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.with_raw_response.list_disk_snapshots() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = await response.parse() + assert_matches_type(AsyncDiskSnapshotsCursorIDPage[DevboxSnapshotView], devbox, path=["response"]) + + @parametrize + async def test_streaming_response_list_disk_snapshots(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.with_streaming_response.list_disk_snapshots() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = await response.parse() + assert_matches_type(AsyncDiskSnapshotsCursorIDPage[DevboxSnapshotView], devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_read_file_contents(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.read_file_contents( + id="id", + file_path="file_path", + ) + assert_matches_type(str, devbox, path=["response"]) + + @parametrize + async def test_raw_response_read_file_contents(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.with_raw_response.read_file_contents( + id="id", + file_path="file_path", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = await response.parse() + assert_matches_type(str, devbox, path=["response"]) + + @parametrize + async def test_streaming_response_read_file_contents(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.with_streaming_response.read_file_contents( + id="id", + file_path="file_path", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = await response.parse() + assert_matches_type(str, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_read_file_contents(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.with_raw_response.read_file_contents( + id="", + file_path="file_path", + ) + + @parametrize + async def test_method_remove_tunnel(self, async_client: AsyncRunloop) -> None: + with pytest.warns(DeprecationWarning): + devbox = await async_client.devboxes.remove_tunnel( + id="id", + port=0, + ) + + assert_matches_type(object, devbox, path=["response"]) + + @parametrize + async def test_raw_response_remove_tunnel(self, async_client: AsyncRunloop) -> None: + with pytest.warns(DeprecationWarning): + response = await async_client.devboxes.with_raw_response.remove_tunnel( + id="id", + port=0, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = await response.parse() + assert_matches_type(object, devbox, path=["response"]) + + @parametrize + async def test_streaming_response_remove_tunnel(self, async_client: AsyncRunloop) -> None: + with pytest.warns(DeprecationWarning): + async with async_client.devboxes.with_streaming_response.remove_tunnel( + id="id", + port=0, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = await response.parse() + assert_matches_type(object, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_remove_tunnel(self, async_client: AsyncRunloop) -> None: + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.with_raw_response.remove_tunnel( + id="", + port=0, + ) + + @parametrize + async def test_method_resume(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.resume( + "id", + ) + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + async def test_raw_response_resume(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.with_raw_response.resume( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = await response.parse() + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + async def test_streaming_response_resume(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.with_streaming_response.resume( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = await response.parse() + assert_matches_type(DevboxView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_resume(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.with_raw_response.resume( + "", + ) + + @parametrize + async def test_method_retrieve_resource_usage(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.retrieve_resource_usage( + "id", + ) + assert_matches_type(DevboxResourceUsageView, devbox, path=["response"]) + + @parametrize + async def test_raw_response_retrieve_resource_usage(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.with_raw_response.retrieve_resource_usage( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = await response.parse() + assert_matches_type(DevboxResourceUsageView, devbox, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve_resource_usage(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.with_streaming_response.retrieve_resource_usage( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = await response.parse() + assert_matches_type(DevboxResourceUsageView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve_resource_usage(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.with_raw_response.retrieve_resource_usage( + "", + ) + + @parametrize + async def test_method_shutdown(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.shutdown( + id="id", + ) + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + async def test_method_shutdown_with_all_params(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.shutdown( + id="id", + force="force", + ) + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + async def test_raw_response_shutdown(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.with_raw_response.shutdown( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = await response.parse() + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + async def test_streaming_response_shutdown(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.with_streaming_response.shutdown( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = await response.parse() + assert_matches_type(DevboxView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_shutdown(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.with_raw_response.shutdown( + id="", + ) + + @parametrize + async def test_method_snapshot_disk(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.snapshot_disk( + id="id", + ) + assert_matches_type(DevboxSnapshotView, devbox, path=["response"]) + + @parametrize + async def test_method_snapshot_disk_with_all_params(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.snapshot_disk( + id="id", + commit_message="commit_message", + metadata={"foo": "string"}, + name="name", + ) + assert_matches_type(DevboxSnapshotView, devbox, path=["response"]) + + @parametrize + async def test_raw_response_snapshot_disk(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.with_raw_response.snapshot_disk( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = await response.parse() + assert_matches_type(DevboxSnapshotView, devbox, path=["response"]) + + @parametrize + async def test_streaming_response_snapshot_disk(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.with_streaming_response.snapshot_disk( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = await response.parse() + assert_matches_type(DevboxSnapshotView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_snapshot_disk(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.with_raw_response.snapshot_disk( + id="", + ) + + @parametrize + async def test_method_snapshot_disk_async(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.snapshot_disk_async( + id="id", + ) + assert_matches_type(DevboxSnapshotView, devbox, path=["response"]) + + @parametrize + async def test_method_snapshot_disk_async_with_all_params(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.snapshot_disk_async( + id="id", + commit_message="commit_message", + metadata={"foo": "string"}, + name="name", + ) + assert_matches_type(DevboxSnapshotView, devbox, path=["response"]) + + @parametrize + async def test_raw_response_snapshot_disk_async(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.with_raw_response.snapshot_disk_async( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = await response.parse() + assert_matches_type(DevboxSnapshotView, devbox, path=["response"]) + + @parametrize + async def test_streaming_response_snapshot_disk_async(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.with_streaming_response.snapshot_disk_async( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = await response.parse() + assert_matches_type(DevboxSnapshotView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_snapshot_disk_async(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.with_raw_response.snapshot_disk_async( + id="", + ) + + @parametrize + async def test_method_suspend(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.suspend( + "id", + ) + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + async def test_raw_response_suspend(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.with_raw_response.suspend( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = await response.parse() + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + async def test_streaming_response_suspend(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.with_streaming_response.suspend( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = await response.parse() + assert_matches_type(DevboxView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_suspend(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.with_raw_response.suspend( + "", + ) + + @parametrize + async def test_method_upload_file(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.upload_file( + id="id", + path="path", + ) + assert_matches_type(object, devbox, path=["response"]) + + @parametrize + async def test_method_upload_file_with_all_params(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.upload_file( + id="id", + path="path", + file=b"Example data", + ) + assert_matches_type(object, devbox, path=["response"]) + + @parametrize + async def test_raw_response_upload_file(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.with_raw_response.upload_file( + id="id", + path="path", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = await response.parse() + assert_matches_type(object, devbox, path=["response"]) + + @parametrize + async def test_streaming_response_upload_file(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.with_streaming_response.upload_file( + id="id", + path="path", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = await response.parse() + assert_matches_type(object, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_upload_file(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.with_raw_response.upload_file( + id="", + path="path", + ) + + @parametrize + async def test_method_wait_for_command(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.wait_for_command( + execution_id="execution_id", + devbox_id="devbox_id", + statuses=["queued"], + ) + assert_matches_type(DevboxAsyncExecutionDetailView, devbox, path=["response"]) + + @parametrize + async def test_method_wait_for_command_with_all_params(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.wait_for_command( + execution_id="execution_id", + devbox_id="devbox_id", + statuses=["queued"], + last_n="last_n", + timeout_seconds=0, + ) + assert_matches_type(DevboxAsyncExecutionDetailView, devbox, path=["response"]) + + @parametrize + async def test_raw_response_wait_for_command(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.with_raw_response.wait_for_command( + execution_id="execution_id", + devbox_id="devbox_id", + statuses=["queued"], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = await response.parse() + assert_matches_type(DevboxAsyncExecutionDetailView, devbox, path=["response"]) + + @parametrize + async def test_streaming_response_wait_for_command(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.with_streaming_response.wait_for_command( + execution_id="execution_id", + devbox_id="devbox_id", + statuses=["queued"], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = await response.parse() + assert_matches_type(DevboxAsyncExecutionDetailView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_wait_for_command(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `devbox_id` but received ''"): + await async_client.devboxes.with_raw_response.wait_for_command( + execution_id="execution_id", + devbox_id="", + statuses=["queued"], + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `execution_id` but received ''"): + await async_client.devboxes.with_raw_response.wait_for_command( + execution_id="", + devbox_id="devbox_id", + statuses=["queued"], + ) + + @parametrize + async def test_method_write_file_contents(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.write_file_contents( + id="id", + contents="contents", + file_path="file_path", + ) + assert_matches_type(DevboxExecutionDetailView, devbox, path=["response"]) + + @parametrize + async def test_raw_response_write_file_contents(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.with_raw_response.write_file_contents( + id="id", + contents="contents", + file_path="file_path", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = await response.parse() + assert_matches_type(DevboxExecutionDetailView, devbox, path=["response"]) + + @parametrize + async def test_streaming_response_write_file_contents(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.with_streaming_response.write_file_contents( + id="id", + contents="contents", + file_path="file_path", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = await response.parse() + assert_matches_type(DevboxExecutionDetailView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_write_file_contents(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.with_raw_response.write_file_contents( + id="", + contents="contents", + file_path="file_path", + ) diff --git a/tests/api_resources/test_gateway_configs.py b/tests/api_resources/test_gateway_configs.py new file mode 100644 index 000000000..6265b9875 --- /dev/null +++ b/tests/api_resources/test_gateway_configs.py @@ -0,0 +1,453 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from runloop_api_client import Runloop, AsyncRunloop +from runloop_api_client.types import ( + GatewayConfigView, +) +from runloop_api_client.pagination import SyncGatewayConfigsCursorIDPage, AsyncGatewayConfigsCursorIDPage + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestGatewayConfigs: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Runloop) -> None: + gateway_config = client.gateway_configs.create( + auth_mechanism={"type": "type"}, + endpoint="endpoint", + name="name", + ) + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: Runloop) -> None: + gateway_config = client.gateway_configs.create( + auth_mechanism={ + "type": "type", + "key": "key", + }, + endpoint="endpoint", + name="name", + description="description", + ) + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Runloop) -> None: + response = client.gateway_configs.with_raw_response.create( + auth_mechanism={"type": "type"}, + endpoint="endpoint", + name="name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + gateway_config = response.parse() + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Runloop) -> None: + with client.gateway_configs.with_streaming_response.create( + auth_mechanism={"type": "type"}, + endpoint="endpoint", + name="name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + gateway_config = response.parse() + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_retrieve(self, client: Runloop) -> None: + gateway_config = client.gateway_configs.retrieve( + "id", + ) + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Runloop) -> None: + response = client.gateway_configs.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + gateway_config = response.parse() + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Runloop) -> None: + with client.gateway_configs.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + gateway_config = response.parse() + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.gateway_configs.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_update(self, client: Runloop) -> None: + gateway_config = client.gateway_configs.update( + id="id", + ) + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + def test_method_update_with_all_params(self, client: Runloop) -> None: + gateway_config = client.gateway_configs.update( + id="id", + auth_mechanism={ + "type": "type", + "key": "key", + }, + description="description", + endpoint="endpoint", + name="name", + ) + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + def test_raw_response_update(self, client: Runloop) -> None: + response = client.gateway_configs.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + gateway_config = response.parse() + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + def test_streaming_response_update(self, client: Runloop) -> None: + with client.gateway_configs.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + gateway_config = response.parse() + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_update(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.gateway_configs.with_raw_response.update( + id="", + ) + + @parametrize + def test_method_list(self, client: Runloop) -> None: + gateway_config = client.gateway_configs.list() + assert_matches_type(SyncGatewayConfigsCursorIDPage[GatewayConfigView], gateway_config, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Runloop) -> None: + gateway_config = client.gateway_configs.list( + id="id", + limit=0, + name="name", + starting_after="starting_after", + ) + assert_matches_type(SyncGatewayConfigsCursorIDPage[GatewayConfigView], gateway_config, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Runloop) -> None: + response = client.gateway_configs.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + gateway_config = response.parse() + assert_matches_type(SyncGatewayConfigsCursorIDPage[GatewayConfigView], gateway_config, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Runloop) -> None: + with client.gateway_configs.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + gateway_config = response.parse() + assert_matches_type(SyncGatewayConfigsCursorIDPage[GatewayConfigView], gateway_config, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_delete(self, client: Runloop) -> None: + gateway_config = client.gateway_configs.delete( + "id", + ) + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + def test_raw_response_delete(self, client: Runloop) -> None: + response = client.gateway_configs.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + gateway_config = response.parse() + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + def test_streaming_response_delete(self, client: Runloop) -> None: + with client.gateway_configs.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + gateway_config = response.parse() + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_delete(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.gateway_configs.with_raw_response.delete( + "", + ) + + +class TestAsyncGatewayConfigs: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_create(self, async_client: AsyncRunloop) -> None: + gateway_config = await async_client.gateway_configs.create( + auth_mechanism={"type": "type"}, + endpoint="endpoint", + name="name", + ) + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncRunloop) -> None: + gateway_config = await async_client.gateway_configs.create( + auth_mechanism={ + "type": "type", + "key": "key", + }, + endpoint="endpoint", + name="name", + description="description", + ) + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncRunloop) -> None: + response = await async_client.gateway_configs.with_raw_response.create( + auth_mechanism={"type": "type"}, + endpoint="endpoint", + name="name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + gateway_config = await response.parse() + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncRunloop) -> None: + async with async_client.gateway_configs.with_streaming_response.create( + auth_mechanism={"type": "type"}, + endpoint="endpoint", + name="name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + gateway_config = await response.parse() + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_retrieve(self, async_client: AsyncRunloop) -> None: + gateway_config = await async_client.gateway_configs.retrieve( + "id", + ) + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncRunloop) -> None: + response = await async_client.gateway_configs.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + gateway_config = await response.parse() + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncRunloop) -> None: + async with async_client.gateway_configs.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + gateway_config = await response.parse() + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.gateway_configs.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_update(self, async_client: AsyncRunloop) -> None: + gateway_config = await async_client.gateway_configs.update( + id="id", + ) + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncRunloop) -> None: + gateway_config = await async_client.gateway_configs.update( + id="id", + auth_mechanism={ + "type": "type", + "key": "key", + }, + description="description", + endpoint="endpoint", + name="name", + ) + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + async def test_raw_response_update(self, async_client: AsyncRunloop) -> None: + response = await async_client.gateway_configs.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + gateway_config = await response.parse() + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + async def test_streaming_response_update(self, async_client: AsyncRunloop) -> None: + async with async_client.gateway_configs.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + gateway_config = await response.parse() + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_update(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.gateway_configs.with_raw_response.update( + id="", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncRunloop) -> None: + gateway_config = await async_client.gateway_configs.list() + assert_matches_type(AsyncGatewayConfigsCursorIDPage[GatewayConfigView], gateway_config, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncRunloop) -> None: + gateway_config = await async_client.gateway_configs.list( + id="id", + limit=0, + name="name", + starting_after="starting_after", + ) + assert_matches_type(AsyncGatewayConfigsCursorIDPage[GatewayConfigView], gateway_config, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncRunloop) -> None: + response = await async_client.gateway_configs.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + gateway_config = await response.parse() + assert_matches_type(AsyncGatewayConfigsCursorIDPage[GatewayConfigView], gateway_config, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncRunloop) -> None: + async with async_client.gateway_configs.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + gateway_config = await response.parse() + assert_matches_type(AsyncGatewayConfigsCursorIDPage[GatewayConfigView], gateway_config, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_delete(self, async_client: AsyncRunloop) -> None: + gateway_config = await async_client.gateway_configs.delete( + "id", + ) + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + async def test_raw_response_delete(self, async_client: AsyncRunloop) -> None: + response = await async_client.gateway_configs.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + gateway_config = await response.parse() + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncRunloop) -> None: + async with async_client.gateway_configs.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + gateway_config = await response.parse() + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_delete(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.gateway_configs.with_raw_response.delete( + "", + ) diff --git a/tests/api_resources/test_mcp_configs.py b/tests/api_resources/test_mcp_configs.py new file mode 100644 index 000000000..0d3135668 --- /dev/null +++ b/tests/api_resources/test_mcp_configs.py @@ -0,0 +1,441 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from runloop_api_client import Runloop, AsyncRunloop +from runloop_api_client.types import ( + McpConfigView, +) +from runloop_api_client.pagination import SyncMcpConfigsCursorIDPage, AsyncMcpConfigsCursorIDPage + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestMcpConfigs: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Runloop) -> None: + mcp_config = client.mcp_configs.create( + allowed_tools=["string"], + endpoint="endpoint", + name="name", + ) + assert_matches_type(McpConfigView, mcp_config, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: Runloop) -> None: + mcp_config = client.mcp_configs.create( + allowed_tools=["string"], + endpoint="endpoint", + name="name", + description="description", + ) + assert_matches_type(McpConfigView, mcp_config, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Runloop) -> None: + response = client.mcp_configs.with_raw_response.create( + allowed_tools=["string"], + endpoint="endpoint", + name="name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + mcp_config = response.parse() + assert_matches_type(McpConfigView, mcp_config, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Runloop) -> None: + with client.mcp_configs.with_streaming_response.create( + allowed_tools=["string"], + endpoint="endpoint", + name="name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + mcp_config = response.parse() + assert_matches_type(McpConfigView, mcp_config, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_retrieve(self, client: Runloop) -> None: + mcp_config = client.mcp_configs.retrieve( + "id", + ) + assert_matches_type(McpConfigView, mcp_config, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Runloop) -> None: + response = client.mcp_configs.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + mcp_config = response.parse() + assert_matches_type(McpConfigView, mcp_config, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Runloop) -> None: + with client.mcp_configs.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + mcp_config = response.parse() + assert_matches_type(McpConfigView, mcp_config, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.mcp_configs.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_update(self, client: Runloop) -> None: + mcp_config = client.mcp_configs.update( + id="id", + ) + assert_matches_type(McpConfigView, mcp_config, path=["response"]) + + @parametrize + def test_method_update_with_all_params(self, client: Runloop) -> None: + mcp_config = client.mcp_configs.update( + id="id", + allowed_tools=["string"], + description="description", + endpoint="endpoint", + name="name", + ) + assert_matches_type(McpConfigView, mcp_config, path=["response"]) + + @parametrize + def test_raw_response_update(self, client: Runloop) -> None: + response = client.mcp_configs.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + mcp_config = response.parse() + assert_matches_type(McpConfigView, mcp_config, path=["response"]) + + @parametrize + def test_streaming_response_update(self, client: Runloop) -> None: + with client.mcp_configs.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + mcp_config = response.parse() + assert_matches_type(McpConfigView, mcp_config, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_update(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.mcp_configs.with_raw_response.update( + id="", + ) + + @parametrize + def test_method_list(self, client: Runloop) -> None: + mcp_config = client.mcp_configs.list() + assert_matches_type(SyncMcpConfigsCursorIDPage[McpConfigView], mcp_config, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Runloop) -> None: + mcp_config = client.mcp_configs.list( + id="id", + limit=0, + name="name", + starting_after="starting_after", + ) + assert_matches_type(SyncMcpConfigsCursorIDPage[McpConfigView], mcp_config, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Runloop) -> None: + response = client.mcp_configs.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + mcp_config = response.parse() + assert_matches_type(SyncMcpConfigsCursorIDPage[McpConfigView], mcp_config, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Runloop) -> None: + with client.mcp_configs.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + mcp_config = response.parse() + assert_matches_type(SyncMcpConfigsCursorIDPage[McpConfigView], mcp_config, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_delete(self, client: Runloop) -> None: + mcp_config = client.mcp_configs.delete( + "id", + ) + assert_matches_type(McpConfigView, mcp_config, path=["response"]) + + @parametrize + def test_raw_response_delete(self, client: Runloop) -> None: + response = client.mcp_configs.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + mcp_config = response.parse() + assert_matches_type(McpConfigView, mcp_config, path=["response"]) + + @parametrize + def test_streaming_response_delete(self, client: Runloop) -> None: + with client.mcp_configs.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + mcp_config = response.parse() + assert_matches_type(McpConfigView, mcp_config, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_delete(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.mcp_configs.with_raw_response.delete( + "", + ) + + +class TestAsyncMcpConfigs: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_create(self, async_client: AsyncRunloop) -> None: + mcp_config = await async_client.mcp_configs.create( + allowed_tools=["string"], + endpoint="endpoint", + name="name", + ) + assert_matches_type(McpConfigView, mcp_config, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncRunloop) -> None: + mcp_config = await async_client.mcp_configs.create( + allowed_tools=["string"], + endpoint="endpoint", + name="name", + description="description", + ) + assert_matches_type(McpConfigView, mcp_config, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncRunloop) -> None: + response = await async_client.mcp_configs.with_raw_response.create( + allowed_tools=["string"], + endpoint="endpoint", + name="name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + mcp_config = await response.parse() + assert_matches_type(McpConfigView, mcp_config, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncRunloop) -> None: + async with async_client.mcp_configs.with_streaming_response.create( + allowed_tools=["string"], + endpoint="endpoint", + name="name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + mcp_config = await response.parse() + assert_matches_type(McpConfigView, mcp_config, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_retrieve(self, async_client: AsyncRunloop) -> None: + mcp_config = await async_client.mcp_configs.retrieve( + "id", + ) + assert_matches_type(McpConfigView, mcp_config, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncRunloop) -> None: + response = await async_client.mcp_configs.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + mcp_config = await response.parse() + assert_matches_type(McpConfigView, mcp_config, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncRunloop) -> None: + async with async_client.mcp_configs.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + mcp_config = await response.parse() + assert_matches_type(McpConfigView, mcp_config, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.mcp_configs.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_update(self, async_client: AsyncRunloop) -> None: + mcp_config = await async_client.mcp_configs.update( + id="id", + ) + assert_matches_type(McpConfigView, mcp_config, path=["response"]) + + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncRunloop) -> None: + mcp_config = await async_client.mcp_configs.update( + id="id", + allowed_tools=["string"], + description="description", + endpoint="endpoint", + name="name", + ) + assert_matches_type(McpConfigView, mcp_config, path=["response"]) + + @parametrize + async def test_raw_response_update(self, async_client: AsyncRunloop) -> None: + response = await async_client.mcp_configs.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + mcp_config = await response.parse() + assert_matches_type(McpConfigView, mcp_config, path=["response"]) + + @parametrize + async def test_streaming_response_update(self, async_client: AsyncRunloop) -> None: + async with async_client.mcp_configs.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + mcp_config = await response.parse() + assert_matches_type(McpConfigView, mcp_config, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_update(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.mcp_configs.with_raw_response.update( + id="", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncRunloop) -> None: + mcp_config = await async_client.mcp_configs.list() + assert_matches_type(AsyncMcpConfigsCursorIDPage[McpConfigView], mcp_config, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncRunloop) -> None: + mcp_config = await async_client.mcp_configs.list( + id="id", + limit=0, + name="name", + starting_after="starting_after", + ) + assert_matches_type(AsyncMcpConfigsCursorIDPage[McpConfigView], mcp_config, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncRunloop) -> None: + response = await async_client.mcp_configs.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + mcp_config = await response.parse() + assert_matches_type(AsyncMcpConfigsCursorIDPage[McpConfigView], mcp_config, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncRunloop) -> None: + async with async_client.mcp_configs.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + mcp_config = await response.parse() + assert_matches_type(AsyncMcpConfigsCursorIDPage[McpConfigView], mcp_config, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_delete(self, async_client: AsyncRunloop) -> None: + mcp_config = await async_client.mcp_configs.delete( + "id", + ) + assert_matches_type(McpConfigView, mcp_config, path=["response"]) + + @parametrize + async def test_raw_response_delete(self, async_client: AsyncRunloop) -> None: + response = await async_client.mcp_configs.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + mcp_config = await response.parse() + assert_matches_type(McpConfigView, mcp_config, path=["response"]) + + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncRunloop) -> None: + async with async_client.mcp_configs.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + mcp_config = await response.parse() + assert_matches_type(McpConfigView, mcp_config, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_delete(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.mcp_configs.with_raw_response.delete( + "", + ) diff --git a/tests/api_resources/test_network_policies.py b/tests/api_resources/test_network_policies.py new file mode 100644 index 000000000..6a1587cb9 --- /dev/null +++ b/tests/api_resources/test_network_policies.py @@ -0,0 +1,441 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from runloop_api_client import Runloop, AsyncRunloop +from runloop_api_client.types import ( + NetworkPolicyView, +) +from runloop_api_client.pagination import SyncNetworkPoliciesCursorIDPage, AsyncNetworkPoliciesCursorIDPage + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestNetworkPolicies: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Runloop) -> None: + network_policy = client.network_policies.create( + name="name", + ) + assert_matches_type(NetworkPolicyView, network_policy, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: Runloop) -> None: + network_policy = client.network_policies.create( + name="name", + allow_agent_gateway=True, + allow_all=True, + allow_devbox_to_devbox=True, + allow_mcp_gateway=True, + allowed_hostnames=["string"], + description="description", + ) + assert_matches_type(NetworkPolicyView, network_policy, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Runloop) -> None: + response = client.network_policies.with_raw_response.create( + name="name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + network_policy = response.parse() + assert_matches_type(NetworkPolicyView, network_policy, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Runloop) -> None: + with client.network_policies.with_streaming_response.create( + name="name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + network_policy = response.parse() + assert_matches_type(NetworkPolicyView, network_policy, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_retrieve(self, client: Runloop) -> None: + network_policy = client.network_policies.retrieve( + "id", + ) + assert_matches_type(NetworkPolicyView, network_policy, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Runloop) -> None: + response = client.network_policies.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + network_policy = response.parse() + assert_matches_type(NetworkPolicyView, network_policy, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Runloop) -> None: + with client.network_policies.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + network_policy = response.parse() + assert_matches_type(NetworkPolicyView, network_policy, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.network_policies.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_update(self, client: Runloop) -> None: + network_policy = client.network_policies.update( + id="id", + ) + assert_matches_type(NetworkPolicyView, network_policy, path=["response"]) + + @parametrize + def test_method_update_with_all_params(self, client: Runloop) -> None: + network_policy = client.network_policies.update( + id="id", + allow_agent_gateway=True, + allow_all=True, + allow_devbox_to_devbox=True, + allow_mcp_gateway=True, + allowed_hostnames=["string"], + description="description", + name="name", + ) + assert_matches_type(NetworkPolicyView, network_policy, path=["response"]) + + @parametrize + def test_raw_response_update(self, client: Runloop) -> None: + response = client.network_policies.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + network_policy = response.parse() + assert_matches_type(NetworkPolicyView, network_policy, path=["response"]) + + @parametrize + def test_streaming_response_update(self, client: Runloop) -> None: + with client.network_policies.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + network_policy = response.parse() + assert_matches_type(NetworkPolicyView, network_policy, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_update(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.network_policies.with_raw_response.update( + id="", + ) + + @parametrize + def test_method_list(self, client: Runloop) -> None: + network_policy = client.network_policies.list() + assert_matches_type(SyncNetworkPoliciesCursorIDPage[NetworkPolicyView], network_policy, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Runloop) -> None: + network_policy = client.network_policies.list( + id="id", + limit=0, + name="name", + starting_after="starting_after", + ) + assert_matches_type(SyncNetworkPoliciesCursorIDPage[NetworkPolicyView], network_policy, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Runloop) -> None: + response = client.network_policies.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + network_policy = response.parse() + assert_matches_type(SyncNetworkPoliciesCursorIDPage[NetworkPolicyView], network_policy, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Runloop) -> None: + with client.network_policies.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + network_policy = response.parse() + assert_matches_type(SyncNetworkPoliciesCursorIDPage[NetworkPolicyView], network_policy, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_delete(self, client: Runloop) -> None: + network_policy = client.network_policies.delete( + "id", + ) + assert_matches_type(NetworkPolicyView, network_policy, path=["response"]) + + @parametrize + def test_raw_response_delete(self, client: Runloop) -> None: + response = client.network_policies.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + network_policy = response.parse() + assert_matches_type(NetworkPolicyView, network_policy, path=["response"]) + + @parametrize + def test_streaming_response_delete(self, client: Runloop) -> None: + with client.network_policies.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + network_policy = response.parse() + assert_matches_type(NetworkPolicyView, network_policy, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_delete(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.network_policies.with_raw_response.delete( + "", + ) + + +class TestAsyncNetworkPolicies: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_create(self, async_client: AsyncRunloop) -> None: + network_policy = await async_client.network_policies.create( + name="name", + ) + assert_matches_type(NetworkPolicyView, network_policy, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncRunloop) -> None: + network_policy = await async_client.network_policies.create( + name="name", + allow_agent_gateway=True, + allow_all=True, + allow_devbox_to_devbox=True, + allow_mcp_gateway=True, + allowed_hostnames=["string"], + description="description", + ) + assert_matches_type(NetworkPolicyView, network_policy, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncRunloop) -> None: + response = await async_client.network_policies.with_raw_response.create( + name="name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + network_policy = await response.parse() + assert_matches_type(NetworkPolicyView, network_policy, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncRunloop) -> None: + async with async_client.network_policies.with_streaming_response.create( + name="name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + network_policy = await response.parse() + assert_matches_type(NetworkPolicyView, network_policy, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_retrieve(self, async_client: AsyncRunloop) -> None: + network_policy = await async_client.network_policies.retrieve( + "id", + ) + assert_matches_type(NetworkPolicyView, network_policy, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncRunloop) -> None: + response = await async_client.network_policies.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + network_policy = await response.parse() + assert_matches_type(NetworkPolicyView, network_policy, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncRunloop) -> None: + async with async_client.network_policies.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + network_policy = await response.parse() + assert_matches_type(NetworkPolicyView, network_policy, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.network_policies.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_update(self, async_client: AsyncRunloop) -> None: + network_policy = await async_client.network_policies.update( + id="id", + ) + assert_matches_type(NetworkPolicyView, network_policy, path=["response"]) + + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncRunloop) -> None: + network_policy = await async_client.network_policies.update( + id="id", + allow_agent_gateway=True, + allow_all=True, + allow_devbox_to_devbox=True, + allow_mcp_gateway=True, + allowed_hostnames=["string"], + description="description", + name="name", + ) + assert_matches_type(NetworkPolicyView, network_policy, path=["response"]) + + @parametrize + async def test_raw_response_update(self, async_client: AsyncRunloop) -> None: + response = await async_client.network_policies.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + network_policy = await response.parse() + assert_matches_type(NetworkPolicyView, network_policy, path=["response"]) + + @parametrize + async def test_streaming_response_update(self, async_client: AsyncRunloop) -> None: + async with async_client.network_policies.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + network_policy = await response.parse() + assert_matches_type(NetworkPolicyView, network_policy, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_update(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.network_policies.with_raw_response.update( + id="", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncRunloop) -> None: + network_policy = await async_client.network_policies.list() + assert_matches_type(AsyncNetworkPoliciesCursorIDPage[NetworkPolicyView], network_policy, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncRunloop) -> None: + network_policy = await async_client.network_policies.list( + id="id", + limit=0, + name="name", + starting_after="starting_after", + ) + assert_matches_type(AsyncNetworkPoliciesCursorIDPage[NetworkPolicyView], network_policy, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncRunloop) -> None: + response = await async_client.network_policies.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + network_policy = await response.parse() + assert_matches_type(AsyncNetworkPoliciesCursorIDPage[NetworkPolicyView], network_policy, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncRunloop) -> None: + async with async_client.network_policies.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + network_policy = await response.parse() + assert_matches_type(AsyncNetworkPoliciesCursorIDPage[NetworkPolicyView], network_policy, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_delete(self, async_client: AsyncRunloop) -> None: + network_policy = await async_client.network_policies.delete( + "id", + ) + assert_matches_type(NetworkPolicyView, network_policy, path=["response"]) + + @parametrize + async def test_raw_response_delete(self, async_client: AsyncRunloop) -> None: + response = await async_client.network_policies.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + network_policy = await response.parse() + assert_matches_type(NetworkPolicyView, network_policy, path=["response"]) + + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncRunloop) -> None: + async with async_client.network_policies.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + network_policy = await response.parse() + assert_matches_type(NetworkPolicyView, network_policy, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_delete(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.network_policies.with_raw_response.delete( + "", + ) diff --git a/tests/api_resources/test_objects.py b/tests/api_resources/test_objects.py new file mode 100644 index 000000000..2593e03bf --- /dev/null +++ b/tests/api_resources/test_objects.py @@ -0,0 +1,584 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from runloop_api_client import Runloop, AsyncRunloop +from runloop_api_client.types import ( + ObjectView, + ObjectDownloadURLView, +) +from runloop_api_client.pagination import SyncObjectsCursorIDPage, AsyncObjectsCursorIDPage + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestObjects: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Runloop) -> None: + object_ = client.objects.create( + content_type="unspecified", + name="name", + ) + assert_matches_type(ObjectView, object_, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: Runloop) -> None: + object_ = client.objects.create( + content_type="unspecified", + name="name", + metadata={"foo": "string"}, + ttl_ms=0, + ) + assert_matches_type(ObjectView, object_, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Runloop) -> None: + response = client.objects.with_raw_response.create( + content_type="unspecified", + name="name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + object_ = response.parse() + assert_matches_type(ObjectView, object_, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Runloop) -> None: + with client.objects.with_streaming_response.create( + content_type="unspecified", + name="name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + object_ = response.parse() + assert_matches_type(ObjectView, object_, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_retrieve(self, client: Runloop) -> None: + object_ = client.objects.retrieve( + "id", + ) + assert_matches_type(ObjectView, object_, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Runloop) -> None: + response = client.objects.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + object_ = response.parse() + assert_matches_type(ObjectView, object_, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Runloop) -> None: + with client.objects.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + object_ = response.parse() + assert_matches_type(ObjectView, object_, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.objects.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_list(self, client: Runloop) -> None: + object_ = client.objects.list() + assert_matches_type(SyncObjectsCursorIDPage[ObjectView], object_, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Runloop) -> None: + object_ = client.objects.list( + content_type="unspecified", + limit=0, + name="name", + search="search", + starting_after="starting_after", + state="UPLOADING", + ) + assert_matches_type(SyncObjectsCursorIDPage[ObjectView], object_, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Runloop) -> None: + response = client.objects.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + object_ = response.parse() + assert_matches_type(SyncObjectsCursorIDPage[ObjectView], object_, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Runloop) -> None: + with client.objects.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + object_ = response.parse() + assert_matches_type(SyncObjectsCursorIDPage[ObjectView], object_, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_delete(self, client: Runloop) -> None: + object_ = client.objects.delete( + "id", + ) + assert_matches_type(ObjectView, object_, path=["response"]) + + @parametrize + def test_raw_response_delete(self, client: Runloop) -> None: + response = client.objects.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + object_ = response.parse() + assert_matches_type(ObjectView, object_, path=["response"]) + + @parametrize + def test_streaming_response_delete(self, client: Runloop) -> None: + with client.objects.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + object_ = response.parse() + assert_matches_type(ObjectView, object_, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_delete(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.objects.with_raw_response.delete( + "", + ) + + @parametrize + def test_method_complete(self, client: Runloop) -> None: + object_ = client.objects.complete( + "id", + ) + assert_matches_type(ObjectView, object_, path=["response"]) + + @parametrize + def test_raw_response_complete(self, client: Runloop) -> None: + response = client.objects.with_raw_response.complete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + object_ = response.parse() + assert_matches_type(ObjectView, object_, path=["response"]) + + @parametrize + def test_streaming_response_complete(self, client: Runloop) -> None: + with client.objects.with_streaming_response.complete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + object_ = response.parse() + assert_matches_type(ObjectView, object_, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_complete(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.objects.with_raw_response.complete( + "", + ) + + @parametrize + def test_method_download(self, client: Runloop) -> None: + object_ = client.objects.download( + id="id", + ) + assert_matches_type(ObjectDownloadURLView, object_, path=["response"]) + + @parametrize + def test_method_download_with_all_params(self, client: Runloop) -> None: + object_ = client.objects.download( + id="id", + duration_seconds=0, + ) + assert_matches_type(ObjectDownloadURLView, object_, path=["response"]) + + @parametrize + def test_raw_response_download(self, client: Runloop) -> None: + response = client.objects.with_raw_response.download( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + object_ = response.parse() + assert_matches_type(ObjectDownloadURLView, object_, path=["response"]) + + @parametrize + def test_streaming_response_download(self, client: Runloop) -> None: + with client.objects.with_streaming_response.download( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + object_ = response.parse() + assert_matches_type(ObjectDownloadURLView, object_, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_download(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.objects.with_raw_response.download( + id="", + ) + + @parametrize + def test_method_list_public(self, client: Runloop) -> None: + object_ = client.objects.list_public() + assert_matches_type(SyncObjectsCursorIDPage[ObjectView], object_, path=["response"]) + + @parametrize + def test_method_list_public_with_all_params(self, client: Runloop) -> None: + object_ = client.objects.list_public( + content_type="unspecified", + limit=0, + name="name", + search="search", + starting_after="starting_after", + state="UPLOADING", + ) + assert_matches_type(SyncObjectsCursorIDPage[ObjectView], object_, path=["response"]) + + @parametrize + def test_raw_response_list_public(self, client: Runloop) -> None: + response = client.objects.with_raw_response.list_public() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + object_ = response.parse() + assert_matches_type(SyncObjectsCursorIDPage[ObjectView], object_, path=["response"]) + + @parametrize + def test_streaming_response_list_public(self, client: Runloop) -> None: + with client.objects.with_streaming_response.list_public() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + object_ = response.parse() + assert_matches_type(SyncObjectsCursorIDPage[ObjectView], object_, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncObjects: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_create(self, async_client: AsyncRunloop) -> None: + object_ = await async_client.objects.create( + content_type="unspecified", + name="name", + ) + assert_matches_type(ObjectView, object_, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncRunloop) -> None: + object_ = await async_client.objects.create( + content_type="unspecified", + name="name", + metadata={"foo": "string"}, + ttl_ms=0, + ) + assert_matches_type(ObjectView, object_, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncRunloop) -> None: + response = await async_client.objects.with_raw_response.create( + content_type="unspecified", + name="name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + object_ = await response.parse() + assert_matches_type(ObjectView, object_, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncRunloop) -> None: + async with async_client.objects.with_streaming_response.create( + content_type="unspecified", + name="name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + object_ = await response.parse() + assert_matches_type(ObjectView, object_, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_retrieve(self, async_client: AsyncRunloop) -> None: + object_ = await async_client.objects.retrieve( + "id", + ) + assert_matches_type(ObjectView, object_, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncRunloop) -> None: + response = await async_client.objects.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + object_ = await response.parse() + assert_matches_type(ObjectView, object_, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncRunloop) -> None: + async with async_client.objects.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + object_ = await response.parse() + assert_matches_type(ObjectView, object_, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.objects.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncRunloop) -> None: + object_ = await async_client.objects.list() + assert_matches_type(AsyncObjectsCursorIDPage[ObjectView], object_, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncRunloop) -> None: + object_ = await async_client.objects.list( + content_type="unspecified", + limit=0, + name="name", + search="search", + starting_after="starting_after", + state="UPLOADING", + ) + assert_matches_type(AsyncObjectsCursorIDPage[ObjectView], object_, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncRunloop) -> None: + response = await async_client.objects.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + object_ = await response.parse() + assert_matches_type(AsyncObjectsCursorIDPage[ObjectView], object_, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncRunloop) -> None: + async with async_client.objects.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + object_ = await response.parse() + assert_matches_type(AsyncObjectsCursorIDPage[ObjectView], object_, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_delete(self, async_client: AsyncRunloop) -> None: + object_ = await async_client.objects.delete( + "id", + ) + assert_matches_type(ObjectView, object_, path=["response"]) + + @parametrize + async def test_raw_response_delete(self, async_client: AsyncRunloop) -> None: + response = await async_client.objects.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + object_ = await response.parse() + assert_matches_type(ObjectView, object_, path=["response"]) + + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncRunloop) -> None: + async with async_client.objects.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + object_ = await response.parse() + assert_matches_type(ObjectView, object_, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_delete(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.objects.with_raw_response.delete( + "", + ) + + @parametrize + async def test_method_complete(self, async_client: AsyncRunloop) -> None: + object_ = await async_client.objects.complete( + "id", + ) + assert_matches_type(ObjectView, object_, path=["response"]) + + @parametrize + async def test_raw_response_complete(self, async_client: AsyncRunloop) -> None: + response = await async_client.objects.with_raw_response.complete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + object_ = await response.parse() + assert_matches_type(ObjectView, object_, path=["response"]) + + @parametrize + async def test_streaming_response_complete(self, async_client: AsyncRunloop) -> None: + async with async_client.objects.with_streaming_response.complete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + object_ = await response.parse() + assert_matches_type(ObjectView, object_, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_complete(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.objects.with_raw_response.complete( + "", + ) + + @parametrize + async def test_method_download(self, async_client: AsyncRunloop) -> None: + object_ = await async_client.objects.download( + id="id", + ) + assert_matches_type(ObjectDownloadURLView, object_, path=["response"]) + + @parametrize + async def test_method_download_with_all_params(self, async_client: AsyncRunloop) -> None: + object_ = await async_client.objects.download( + id="id", + duration_seconds=0, + ) + assert_matches_type(ObjectDownloadURLView, object_, path=["response"]) + + @parametrize + async def test_raw_response_download(self, async_client: AsyncRunloop) -> None: + response = await async_client.objects.with_raw_response.download( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + object_ = await response.parse() + assert_matches_type(ObjectDownloadURLView, object_, path=["response"]) + + @parametrize + async def test_streaming_response_download(self, async_client: AsyncRunloop) -> None: + async with async_client.objects.with_streaming_response.download( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + object_ = await response.parse() + assert_matches_type(ObjectDownloadURLView, object_, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_download(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.objects.with_raw_response.download( + id="", + ) + + @parametrize + async def test_method_list_public(self, async_client: AsyncRunloop) -> None: + object_ = await async_client.objects.list_public() + assert_matches_type(AsyncObjectsCursorIDPage[ObjectView], object_, path=["response"]) + + @parametrize + async def test_method_list_public_with_all_params(self, async_client: AsyncRunloop) -> None: + object_ = await async_client.objects.list_public( + content_type="unspecified", + limit=0, + name="name", + search="search", + starting_after="starting_after", + state="UPLOADING", + ) + assert_matches_type(AsyncObjectsCursorIDPage[ObjectView], object_, path=["response"]) + + @parametrize + async def test_raw_response_list_public(self, async_client: AsyncRunloop) -> None: + response = await async_client.objects.with_raw_response.list_public() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + object_ = await response.parse() + assert_matches_type(AsyncObjectsCursorIDPage[ObjectView], object_, path=["response"]) + + @parametrize + async def test_streaming_response_list_public(self, async_client: AsyncRunloop) -> None: + async with async_client.objects.with_streaming_response.list_public() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + object_ = await response.parse() + assert_matches_type(AsyncObjectsCursorIDPage[ObjectView], object_, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_repositories.py b/tests/api_resources/test_repositories.py new file mode 100644 index 000000000..282688d80 --- /dev/null +++ b/tests/api_resources/test_repositories.py @@ -0,0 +1,693 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from runloop_api_client import Runloop, AsyncRunloop +from runloop_api_client.types import ( + RepositoryConnectionView, + RepositoryInspectionDetails, + RepositoryInspectionListView, +) +from runloop_api_client.pagination import SyncRepositoriesCursorIDPage, AsyncRepositoriesCursorIDPage + +# pyright: reportDeprecated=false + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestRepositories: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Runloop) -> None: + repository = client.repositories.create( + name="name", + owner="owner", + ) + assert_matches_type(RepositoryConnectionView, repository, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: Runloop) -> None: + repository = client.repositories.create( + name="name", + owner="owner", + blueprint_id="blueprint_id", + github_auth_token="github_auth_token", + ) + assert_matches_type(RepositoryConnectionView, repository, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Runloop) -> None: + response = client.repositories.with_raw_response.create( + name="name", + owner="owner", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + repository = response.parse() + assert_matches_type(RepositoryConnectionView, repository, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Runloop) -> None: + with client.repositories.with_streaming_response.create( + name="name", + owner="owner", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + repository = response.parse() + assert_matches_type(RepositoryConnectionView, repository, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_retrieve(self, client: Runloop) -> None: + repository = client.repositories.retrieve( + "id", + ) + assert_matches_type(RepositoryConnectionView, repository, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Runloop) -> None: + response = client.repositories.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + repository = response.parse() + assert_matches_type(RepositoryConnectionView, repository, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Runloop) -> None: + with client.repositories.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + repository = response.parse() + assert_matches_type(RepositoryConnectionView, repository, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.repositories.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_list(self, client: Runloop) -> None: + repository = client.repositories.list() + assert_matches_type(SyncRepositoriesCursorIDPage[RepositoryConnectionView], repository, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Runloop) -> None: + repository = client.repositories.list( + limit=0, + name="name", + owner="owner", + starting_after="starting_after", + ) + assert_matches_type(SyncRepositoriesCursorIDPage[RepositoryConnectionView], repository, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Runloop) -> None: + response = client.repositories.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + repository = response.parse() + assert_matches_type(SyncRepositoriesCursorIDPage[RepositoryConnectionView], repository, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Runloop) -> None: + with client.repositories.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + repository = response.parse() + assert_matches_type(SyncRepositoriesCursorIDPage[RepositoryConnectionView], repository, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_delete(self, client: Runloop) -> None: + repository = client.repositories.delete( + "id", + ) + assert_matches_type(object, repository, path=["response"]) + + @parametrize + def test_raw_response_delete(self, client: Runloop) -> None: + response = client.repositories.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + repository = response.parse() + assert_matches_type(object, repository, path=["response"]) + + @parametrize + def test_streaming_response_delete(self, client: Runloop) -> None: + with client.repositories.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + repository = response.parse() + assert_matches_type(object, repository, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_delete(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.repositories.with_raw_response.delete( + "", + ) + + @parametrize + def test_method_inspect(self, client: Runloop) -> None: + repository = client.repositories.inspect( + id="id", + ) + assert_matches_type(RepositoryInspectionDetails, repository, path=["response"]) + + @parametrize + def test_method_inspect_with_all_params(self, client: Runloop) -> None: + repository = client.repositories.inspect( + id="id", + github_auth_token="github_auth_token", + ) + assert_matches_type(RepositoryInspectionDetails, repository, path=["response"]) + + @parametrize + def test_raw_response_inspect(self, client: Runloop) -> None: + response = client.repositories.with_raw_response.inspect( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + repository = response.parse() + assert_matches_type(RepositoryInspectionDetails, repository, path=["response"]) + + @parametrize + def test_streaming_response_inspect(self, client: Runloop) -> None: + with client.repositories.with_streaming_response.inspect( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + repository = response.parse() + assert_matches_type(RepositoryInspectionDetails, repository, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_inspect(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.repositories.with_raw_response.inspect( + id="", + ) + + @parametrize + def test_method_list_inspections(self, client: Runloop) -> None: + repository = client.repositories.list_inspections( + "id", + ) + assert_matches_type(RepositoryInspectionListView, repository, path=["response"]) + + @parametrize + def test_raw_response_list_inspections(self, client: Runloop) -> None: + response = client.repositories.with_raw_response.list_inspections( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + repository = response.parse() + assert_matches_type(RepositoryInspectionListView, repository, path=["response"]) + + @parametrize + def test_streaming_response_list_inspections(self, client: Runloop) -> None: + with client.repositories.with_streaming_response.list_inspections( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + repository = response.parse() + assert_matches_type(RepositoryInspectionListView, repository, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_list_inspections(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.repositories.with_raw_response.list_inspections( + "", + ) + + @parametrize + def test_method_refresh(self, client: Runloop) -> None: + with pytest.warns(DeprecationWarning): + repository = client.repositories.refresh( + id="id", + ) + + assert_matches_type(object, repository, path=["response"]) + + @parametrize + def test_method_refresh_with_all_params(self, client: Runloop) -> None: + with pytest.warns(DeprecationWarning): + repository = client.repositories.refresh( + id="id", + blueprint_id="blueprint_id", + github_auth_token="github_auth_token", + ) + + assert_matches_type(object, repository, path=["response"]) + + @parametrize + def test_raw_response_refresh(self, client: Runloop) -> None: + with pytest.warns(DeprecationWarning): + response = client.repositories.with_raw_response.refresh( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + repository = response.parse() + assert_matches_type(object, repository, path=["response"]) + + @parametrize + def test_streaming_response_refresh(self, client: Runloop) -> None: + with pytest.warns(DeprecationWarning): + with client.repositories.with_streaming_response.refresh( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + repository = response.parse() + assert_matches_type(object, repository, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_refresh(self, client: Runloop) -> None: + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.repositories.with_raw_response.refresh( + id="", + ) + + @parametrize + def test_method_retrieve_inspection(self, client: Runloop) -> None: + repository = client.repositories.retrieve_inspection( + "id", + ) + assert_matches_type(RepositoryInspectionDetails, repository, path=["response"]) + + @parametrize + def test_raw_response_retrieve_inspection(self, client: Runloop) -> None: + response = client.repositories.with_raw_response.retrieve_inspection( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + repository = response.parse() + assert_matches_type(RepositoryInspectionDetails, repository, path=["response"]) + + @parametrize + def test_streaming_response_retrieve_inspection(self, client: Runloop) -> None: + with client.repositories.with_streaming_response.retrieve_inspection( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + repository = response.parse() + assert_matches_type(RepositoryInspectionDetails, repository, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve_inspection(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.repositories.with_raw_response.retrieve_inspection( + "", + ) + + +class TestAsyncRepositories: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_create(self, async_client: AsyncRunloop) -> None: + repository = await async_client.repositories.create( + name="name", + owner="owner", + ) + assert_matches_type(RepositoryConnectionView, repository, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncRunloop) -> None: + repository = await async_client.repositories.create( + name="name", + owner="owner", + blueprint_id="blueprint_id", + github_auth_token="github_auth_token", + ) + assert_matches_type(RepositoryConnectionView, repository, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncRunloop) -> None: + response = await async_client.repositories.with_raw_response.create( + name="name", + owner="owner", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + repository = await response.parse() + assert_matches_type(RepositoryConnectionView, repository, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncRunloop) -> None: + async with async_client.repositories.with_streaming_response.create( + name="name", + owner="owner", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + repository = await response.parse() + assert_matches_type(RepositoryConnectionView, repository, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_retrieve(self, async_client: AsyncRunloop) -> None: + repository = await async_client.repositories.retrieve( + "id", + ) + assert_matches_type(RepositoryConnectionView, repository, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncRunloop) -> None: + response = await async_client.repositories.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + repository = await response.parse() + assert_matches_type(RepositoryConnectionView, repository, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncRunloop) -> None: + async with async_client.repositories.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + repository = await response.parse() + assert_matches_type(RepositoryConnectionView, repository, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.repositories.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncRunloop) -> None: + repository = await async_client.repositories.list() + assert_matches_type(AsyncRepositoriesCursorIDPage[RepositoryConnectionView], repository, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncRunloop) -> None: + repository = await async_client.repositories.list( + limit=0, + name="name", + owner="owner", + starting_after="starting_after", + ) + assert_matches_type(AsyncRepositoriesCursorIDPage[RepositoryConnectionView], repository, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncRunloop) -> None: + response = await async_client.repositories.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + repository = await response.parse() + assert_matches_type(AsyncRepositoriesCursorIDPage[RepositoryConnectionView], repository, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncRunloop) -> None: + async with async_client.repositories.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + repository = await response.parse() + assert_matches_type(AsyncRepositoriesCursorIDPage[RepositoryConnectionView], repository, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_delete(self, async_client: AsyncRunloop) -> None: + repository = await async_client.repositories.delete( + "id", + ) + assert_matches_type(object, repository, path=["response"]) + + @parametrize + async def test_raw_response_delete(self, async_client: AsyncRunloop) -> None: + response = await async_client.repositories.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + repository = await response.parse() + assert_matches_type(object, repository, path=["response"]) + + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncRunloop) -> None: + async with async_client.repositories.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + repository = await response.parse() + assert_matches_type(object, repository, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_delete(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.repositories.with_raw_response.delete( + "", + ) + + @parametrize + async def test_method_inspect(self, async_client: AsyncRunloop) -> None: + repository = await async_client.repositories.inspect( + id="id", + ) + assert_matches_type(RepositoryInspectionDetails, repository, path=["response"]) + + @parametrize + async def test_method_inspect_with_all_params(self, async_client: AsyncRunloop) -> None: + repository = await async_client.repositories.inspect( + id="id", + github_auth_token="github_auth_token", + ) + assert_matches_type(RepositoryInspectionDetails, repository, path=["response"]) + + @parametrize + async def test_raw_response_inspect(self, async_client: AsyncRunloop) -> None: + response = await async_client.repositories.with_raw_response.inspect( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + repository = await response.parse() + assert_matches_type(RepositoryInspectionDetails, repository, path=["response"]) + + @parametrize + async def test_streaming_response_inspect(self, async_client: AsyncRunloop) -> None: + async with async_client.repositories.with_streaming_response.inspect( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + repository = await response.parse() + assert_matches_type(RepositoryInspectionDetails, repository, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_inspect(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.repositories.with_raw_response.inspect( + id="", + ) + + @parametrize + async def test_method_list_inspections(self, async_client: AsyncRunloop) -> None: + repository = await async_client.repositories.list_inspections( + "id", + ) + assert_matches_type(RepositoryInspectionListView, repository, path=["response"]) + + @parametrize + async def test_raw_response_list_inspections(self, async_client: AsyncRunloop) -> None: + response = await async_client.repositories.with_raw_response.list_inspections( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + repository = await response.parse() + assert_matches_type(RepositoryInspectionListView, repository, path=["response"]) + + @parametrize + async def test_streaming_response_list_inspections(self, async_client: AsyncRunloop) -> None: + async with async_client.repositories.with_streaming_response.list_inspections( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + repository = await response.parse() + assert_matches_type(RepositoryInspectionListView, repository, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_list_inspections(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.repositories.with_raw_response.list_inspections( + "", + ) + + @parametrize + async def test_method_refresh(self, async_client: AsyncRunloop) -> None: + with pytest.warns(DeprecationWarning): + repository = await async_client.repositories.refresh( + id="id", + ) + + assert_matches_type(object, repository, path=["response"]) + + @parametrize + async def test_method_refresh_with_all_params(self, async_client: AsyncRunloop) -> None: + with pytest.warns(DeprecationWarning): + repository = await async_client.repositories.refresh( + id="id", + blueprint_id="blueprint_id", + github_auth_token="github_auth_token", + ) + + assert_matches_type(object, repository, path=["response"]) + + @parametrize + async def test_raw_response_refresh(self, async_client: AsyncRunloop) -> None: + with pytest.warns(DeprecationWarning): + response = await async_client.repositories.with_raw_response.refresh( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + repository = await response.parse() + assert_matches_type(object, repository, path=["response"]) + + @parametrize + async def test_streaming_response_refresh(self, async_client: AsyncRunloop) -> None: + with pytest.warns(DeprecationWarning): + async with async_client.repositories.with_streaming_response.refresh( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + repository = await response.parse() + assert_matches_type(object, repository, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_refresh(self, async_client: AsyncRunloop) -> None: + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.repositories.with_raw_response.refresh( + id="", + ) + + @parametrize + async def test_method_retrieve_inspection(self, async_client: AsyncRunloop) -> None: + repository = await async_client.repositories.retrieve_inspection( + "id", + ) + assert_matches_type(RepositoryInspectionDetails, repository, path=["response"]) + + @parametrize + async def test_raw_response_retrieve_inspection(self, async_client: AsyncRunloop) -> None: + response = await async_client.repositories.with_raw_response.retrieve_inspection( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + repository = await response.parse() + assert_matches_type(RepositoryInspectionDetails, repository, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve_inspection(self, async_client: AsyncRunloop) -> None: + async with async_client.repositories.with_streaming_response.retrieve_inspection( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + repository = await response.parse() + assert_matches_type(RepositoryInspectionDetails, repository, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve_inspection(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.repositories.with_raw_response.retrieve_inspection( + "", + ) diff --git a/tests/api_resources/test_scenarios.py b/tests/api_resources/test_scenarios.py new file mode 100644 index 000000000..736a8395c --- /dev/null +++ b/tests/api_resources/test_scenarios.py @@ -0,0 +1,914 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from runloop_api_client import Runloop, AsyncRunloop +from runloop_api_client.types import ( + ScenarioView, + ScenarioRunView, +) +from runloop_api_client.pagination import SyncScenariosCursorIDPage, AsyncScenariosCursorIDPage + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestScenarios: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Runloop) -> None: + scenario = client.scenarios.create( + input_context={"problem_statement": "problem_statement"}, + name="name", + scoring_contract={ + "scoring_function_parameters": [ + { + "name": "name", + "scorer": { + "pattern": "pattern", + "search_directory": "search_directory", + "type": "ast_grep_scorer", + }, + "weight": 0, + } + ] + }, + ) + assert_matches_type(ScenarioView, scenario, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: Runloop) -> None: + scenario = client.scenarios.create( + input_context={ + "problem_statement": "problem_statement", + "additional_context": {}, + }, + name="name", + scoring_contract={ + "scoring_function_parameters": [ + { + "name": "name", + "scorer": { + "pattern": "pattern", + "search_directory": "search_directory", + "type": "ast_grep_scorer", + "lang": "lang", + }, + "weight": 0, + } + ] + }, + environment_parameters={ + "blueprint_id": "blueprint_id", + "launch_parameters": { + "after_idle": { + "idle_time_seconds": 0, + "on_idle": "shutdown", + }, + "architecture": "x86_64", + "available_ports": [0], + "custom_cpu_cores": 0, + "custom_disk_size": 0, + "custom_gb_memory": 0, + "keep_alive_time_seconds": 0, + "launch_commands": ["string"], + "network_policy_id": "network_policy_id", + "required_services": ["string"], + "resource_size_request": "X_SMALL", + "user_parameters": { + "uid": 0, + "username": "username", + }, + }, + "snapshot_id": "snapshot_id", + "working_directory": "working_directory", + }, + metadata={"foo": "string"}, + reference_output="reference_output", + required_environment_variables=["string"], + required_secret_names=["string"], + scorer_timeout_sec=0, + validation_type="UNSPECIFIED", + ) + assert_matches_type(ScenarioView, scenario, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Runloop) -> None: + response = client.scenarios.with_raw_response.create( + input_context={"problem_statement": "problem_statement"}, + name="name", + scoring_contract={ + "scoring_function_parameters": [ + { + "name": "name", + "scorer": { + "pattern": "pattern", + "search_directory": "search_directory", + "type": "ast_grep_scorer", + }, + "weight": 0, + } + ] + }, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + scenario = response.parse() + assert_matches_type(ScenarioView, scenario, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Runloop) -> None: + with client.scenarios.with_streaming_response.create( + input_context={"problem_statement": "problem_statement"}, + name="name", + scoring_contract={ + "scoring_function_parameters": [ + { + "name": "name", + "scorer": { + "pattern": "pattern", + "search_directory": "search_directory", + "type": "ast_grep_scorer", + }, + "weight": 0, + } + ] + }, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + scenario = response.parse() + assert_matches_type(ScenarioView, scenario, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_retrieve(self, client: Runloop) -> None: + scenario = client.scenarios.retrieve( + "id", + ) + assert_matches_type(ScenarioView, scenario, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Runloop) -> None: + response = client.scenarios.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + scenario = response.parse() + assert_matches_type(ScenarioView, scenario, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Runloop) -> None: + with client.scenarios.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + scenario = response.parse() + assert_matches_type(ScenarioView, scenario, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.scenarios.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_update(self, client: Runloop) -> None: + scenario = client.scenarios.update( + id="id", + ) + assert_matches_type(ScenarioView, scenario, path=["response"]) + + @parametrize + def test_method_update_with_all_params(self, client: Runloop) -> None: + scenario = client.scenarios.update( + id="id", + environment_parameters={ + "blueprint_id": "blueprint_id", + "launch_parameters": { + "after_idle": { + "idle_time_seconds": 0, + "on_idle": "shutdown", + }, + "architecture": "x86_64", + "available_ports": [0], + "custom_cpu_cores": 0, + "custom_disk_size": 0, + "custom_gb_memory": 0, + "keep_alive_time_seconds": 0, + "launch_commands": ["string"], + "network_policy_id": "network_policy_id", + "required_services": ["string"], + "resource_size_request": "X_SMALL", + "user_parameters": { + "uid": 0, + "username": "username", + }, + }, + "snapshot_id": "snapshot_id", + "working_directory": "working_directory", + }, + input_context={ + "additional_context": {}, + "problem_statement": "problem_statement", + }, + metadata={"foo": "string"}, + name="name", + reference_output="reference_output", + required_environment_variables=["string"], + required_secret_names=["string"], + scorer_timeout_sec=0, + scoring_contract={ + "scoring_function_parameters": [ + { + "name": "name", + "scorer": { + "pattern": "pattern", + "search_directory": "search_directory", + "type": "ast_grep_scorer", + "lang": "lang", + }, + "weight": 0, + } + ] + }, + validation_type="UNSPECIFIED", + ) + assert_matches_type(ScenarioView, scenario, path=["response"]) + + @parametrize + def test_raw_response_update(self, client: Runloop) -> None: + response = client.scenarios.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + scenario = response.parse() + assert_matches_type(ScenarioView, scenario, path=["response"]) + + @parametrize + def test_streaming_response_update(self, client: Runloop) -> None: + with client.scenarios.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + scenario = response.parse() + assert_matches_type(ScenarioView, scenario, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_update(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.scenarios.with_raw_response.update( + id="", + ) + + @parametrize + def test_method_list(self, client: Runloop) -> None: + scenario = client.scenarios.list() + assert_matches_type(SyncScenariosCursorIDPage[ScenarioView], scenario, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Runloop) -> None: + scenario = client.scenarios.list( + benchmark_id="benchmark_id", + limit=0, + name="name", + starting_after="starting_after", + validation_type="validation_type", + ) + assert_matches_type(SyncScenariosCursorIDPage[ScenarioView], scenario, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Runloop) -> None: + response = client.scenarios.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + scenario = response.parse() + assert_matches_type(SyncScenariosCursorIDPage[ScenarioView], scenario, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Runloop) -> None: + with client.scenarios.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + scenario = response.parse() + assert_matches_type(SyncScenariosCursorIDPage[ScenarioView], scenario, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_archive(self, client: Runloop) -> None: + scenario = client.scenarios.archive( + "id", + ) + assert_matches_type(ScenarioView, scenario, path=["response"]) + + @parametrize + def test_raw_response_archive(self, client: Runloop) -> None: + response = client.scenarios.with_raw_response.archive( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + scenario = response.parse() + assert_matches_type(ScenarioView, scenario, path=["response"]) + + @parametrize + def test_streaming_response_archive(self, client: Runloop) -> None: + with client.scenarios.with_streaming_response.archive( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + scenario = response.parse() + assert_matches_type(ScenarioView, scenario, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_archive(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.scenarios.with_raw_response.archive( + "", + ) + + @parametrize + def test_method_list_public(self, client: Runloop) -> None: + scenario = client.scenarios.list_public() + assert_matches_type(SyncScenariosCursorIDPage[ScenarioView], scenario, path=["response"]) + + @parametrize + def test_method_list_public_with_all_params(self, client: Runloop) -> None: + scenario = client.scenarios.list_public( + limit=0, + name="name", + starting_after="starting_after", + ) + assert_matches_type(SyncScenariosCursorIDPage[ScenarioView], scenario, path=["response"]) + + @parametrize + def test_raw_response_list_public(self, client: Runloop) -> None: + response = client.scenarios.with_raw_response.list_public() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + scenario = response.parse() + assert_matches_type(SyncScenariosCursorIDPage[ScenarioView], scenario, path=["response"]) + + @parametrize + def test_streaming_response_list_public(self, client: Runloop) -> None: + with client.scenarios.with_streaming_response.list_public() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + scenario = response.parse() + assert_matches_type(SyncScenariosCursorIDPage[ScenarioView], scenario, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_start_run(self, client: Runloop) -> None: + scenario = client.scenarios.start_run( + scenario_id="scenario_id", + ) + assert_matches_type(ScenarioRunView, scenario, path=["response"]) + + @parametrize + def test_method_start_run_with_all_params(self, client: Runloop) -> None: + scenario = client.scenarios.start_run( + scenario_id="scenario_id", + benchmark_run_id="benchmark_run_id", + metadata={"foo": "string"}, + run_name="run_name", + run_profile={ + "env_vars": {"foo": "string"}, + "launch_parameters": { + "after_idle": { + "idle_time_seconds": 0, + "on_idle": "shutdown", + }, + "architecture": "x86_64", + "available_ports": [0], + "custom_cpu_cores": 0, + "custom_disk_size": 0, + "custom_gb_memory": 0, + "keep_alive_time_seconds": 0, + "launch_commands": ["string"], + "network_policy_id": "network_policy_id", + "required_services": ["string"], + "resource_size_request": "X_SMALL", + "user_parameters": { + "uid": 0, + "username": "username", + }, + }, + "mounts": [ + { + "object_id": "object_id", + "object_path": "object_path", + "type": "object_mount", + } + ], + "purpose": "purpose", + "secrets": {"foo": "string"}, + }, + ) + assert_matches_type(ScenarioRunView, scenario, path=["response"]) + + @parametrize + def test_raw_response_start_run(self, client: Runloop) -> None: + response = client.scenarios.with_raw_response.start_run( + scenario_id="scenario_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + scenario = response.parse() + assert_matches_type(ScenarioRunView, scenario, path=["response"]) + + @parametrize + def test_streaming_response_start_run(self, client: Runloop) -> None: + with client.scenarios.with_streaming_response.start_run( + scenario_id="scenario_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + scenario = response.parse() + assert_matches_type(ScenarioRunView, scenario, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncScenarios: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_create(self, async_client: AsyncRunloop) -> None: + scenario = await async_client.scenarios.create( + input_context={"problem_statement": "problem_statement"}, + name="name", + scoring_contract={ + "scoring_function_parameters": [ + { + "name": "name", + "scorer": { + "pattern": "pattern", + "search_directory": "search_directory", + "type": "ast_grep_scorer", + }, + "weight": 0, + } + ] + }, + ) + assert_matches_type(ScenarioView, scenario, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncRunloop) -> None: + scenario = await async_client.scenarios.create( + input_context={ + "problem_statement": "problem_statement", + "additional_context": {}, + }, + name="name", + scoring_contract={ + "scoring_function_parameters": [ + { + "name": "name", + "scorer": { + "pattern": "pattern", + "search_directory": "search_directory", + "type": "ast_grep_scorer", + "lang": "lang", + }, + "weight": 0, + } + ] + }, + environment_parameters={ + "blueprint_id": "blueprint_id", + "launch_parameters": { + "after_idle": { + "idle_time_seconds": 0, + "on_idle": "shutdown", + }, + "architecture": "x86_64", + "available_ports": [0], + "custom_cpu_cores": 0, + "custom_disk_size": 0, + "custom_gb_memory": 0, + "keep_alive_time_seconds": 0, + "launch_commands": ["string"], + "network_policy_id": "network_policy_id", + "required_services": ["string"], + "resource_size_request": "X_SMALL", + "user_parameters": { + "uid": 0, + "username": "username", + }, + }, + "snapshot_id": "snapshot_id", + "working_directory": "working_directory", + }, + metadata={"foo": "string"}, + reference_output="reference_output", + required_environment_variables=["string"], + required_secret_names=["string"], + scorer_timeout_sec=0, + validation_type="UNSPECIFIED", + ) + assert_matches_type(ScenarioView, scenario, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncRunloop) -> None: + response = await async_client.scenarios.with_raw_response.create( + input_context={"problem_statement": "problem_statement"}, + name="name", + scoring_contract={ + "scoring_function_parameters": [ + { + "name": "name", + "scorer": { + "pattern": "pattern", + "search_directory": "search_directory", + "type": "ast_grep_scorer", + }, + "weight": 0, + } + ] + }, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + scenario = await response.parse() + assert_matches_type(ScenarioView, scenario, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncRunloop) -> None: + async with async_client.scenarios.with_streaming_response.create( + input_context={"problem_statement": "problem_statement"}, + name="name", + scoring_contract={ + "scoring_function_parameters": [ + { + "name": "name", + "scorer": { + "pattern": "pattern", + "search_directory": "search_directory", + "type": "ast_grep_scorer", + }, + "weight": 0, + } + ] + }, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + scenario = await response.parse() + assert_matches_type(ScenarioView, scenario, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_retrieve(self, async_client: AsyncRunloop) -> None: + scenario = await async_client.scenarios.retrieve( + "id", + ) + assert_matches_type(ScenarioView, scenario, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncRunloop) -> None: + response = await async_client.scenarios.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + scenario = await response.parse() + assert_matches_type(ScenarioView, scenario, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncRunloop) -> None: + async with async_client.scenarios.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + scenario = await response.parse() + assert_matches_type(ScenarioView, scenario, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.scenarios.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_update(self, async_client: AsyncRunloop) -> None: + scenario = await async_client.scenarios.update( + id="id", + ) + assert_matches_type(ScenarioView, scenario, path=["response"]) + + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncRunloop) -> None: + scenario = await async_client.scenarios.update( + id="id", + environment_parameters={ + "blueprint_id": "blueprint_id", + "launch_parameters": { + "after_idle": { + "idle_time_seconds": 0, + "on_idle": "shutdown", + }, + "architecture": "x86_64", + "available_ports": [0], + "custom_cpu_cores": 0, + "custom_disk_size": 0, + "custom_gb_memory": 0, + "keep_alive_time_seconds": 0, + "launch_commands": ["string"], + "network_policy_id": "network_policy_id", + "required_services": ["string"], + "resource_size_request": "X_SMALL", + "user_parameters": { + "uid": 0, + "username": "username", + }, + }, + "snapshot_id": "snapshot_id", + "working_directory": "working_directory", + }, + input_context={ + "additional_context": {}, + "problem_statement": "problem_statement", + }, + metadata={"foo": "string"}, + name="name", + reference_output="reference_output", + required_environment_variables=["string"], + required_secret_names=["string"], + scorer_timeout_sec=0, + scoring_contract={ + "scoring_function_parameters": [ + { + "name": "name", + "scorer": { + "pattern": "pattern", + "search_directory": "search_directory", + "type": "ast_grep_scorer", + "lang": "lang", + }, + "weight": 0, + } + ] + }, + validation_type="UNSPECIFIED", + ) + assert_matches_type(ScenarioView, scenario, path=["response"]) + + @parametrize + async def test_raw_response_update(self, async_client: AsyncRunloop) -> None: + response = await async_client.scenarios.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + scenario = await response.parse() + assert_matches_type(ScenarioView, scenario, path=["response"]) + + @parametrize + async def test_streaming_response_update(self, async_client: AsyncRunloop) -> None: + async with async_client.scenarios.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + scenario = await response.parse() + assert_matches_type(ScenarioView, scenario, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_update(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.scenarios.with_raw_response.update( + id="", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncRunloop) -> None: + scenario = await async_client.scenarios.list() + assert_matches_type(AsyncScenariosCursorIDPage[ScenarioView], scenario, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncRunloop) -> None: + scenario = await async_client.scenarios.list( + benchmark_id="benchmark_id", + limit=0, + name="name", + starting_after="starting_after", + validation_type="validation_type", + ) + assert_matches_type(AsyncScenariosCursorIDPage[ScenarioView], scenario, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncRunloop) -> None: + response = await async_client.scenarios.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + scenario = await response.parse() + assert_matches_type(AsyncScenariosCursorIDPage[ScenarioView], scenario, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncRunloop) -> None: + async with async_client.scenarios.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + scenario = await response.parse() + assert_matches_type(AsyncScenariosCursorIDPage[ScenarioView], scenario, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_archive(self, async_client: AsyncRunloop) -> None: + scenario = await async_client.scenarios.archive( + "id", + ) + assert_matches_type(ScenarioView, scenario, path=["response"]) + + @parametrize + async def test_raw_response_archive(self, async_client: AsyncRunloop) -> None: + response = await async_client.scenarios.with_raw_response.archive( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + scenario = await response.parse() + assert_matches_type(ScenarioView, scenario, path=["response"]) + + @parametrize + async def test_streaming_response_archive(self, async_client: AsyncRunloop) -> None: + async with async_client.scenarios.with_streaming_response.archive( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + scenario = await response.parse() + assert_matches_type(ScenarioView, scenario, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_archive(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.scenarios.with_raw_response.archive( + "", + ) + + @parametrize + async def test_method_list_public(self, async_client: AsyncRunloop) -> None: + scenario = await async_client.scenarios.list_public() + assert_matches_type(AsyncScenariosCursorIDPage[ScenarioView], scenario, path=["response"]) + + @parametrize + async def test_method_list_public_with_all_params(self, async_client: AsyncRunloop) -> None: + scenario = await async_client.scenarios.list_public( + limit=0, + name="name", + starting_after="starting_after", + ) + assert_matches_type(AsyncScenariosCursorIDPage[ScenarioView], scenario, path=["response"]) + + @parametrize + async def test_raw_response_list_public(self, async_client: AsyncRunloop) -> None: + response = await async_client.scenarios.with_raw_response.list_public() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + scenario = await response.parse() + assert_matches_type(AsyncScenariosCursorIDPage[ScenarioView], scenario, path=["response"]) + + @parametrize + async def test_streaming_response_list_public(self, async_client: AsyncRunloop) -> None: + async with async_client.scenarios.with_streaming_response.list_public() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + scenario = await response.parse() + assert_matches_type(AsyncScenariosCursorIDPage[ScenarioView], scenario, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_start_run(self, async_client: AsyncRunloop) -> None: + scenario = await async_client.scenarios.start_run( + scenario_id="scenario_id", + ) + assert_matches_type(ScenarioRunView, scenario, path=["response"]) + + @parametrize + async def test_method_start_run_with_all_params(self, async_client: AsyncRunloop) -> None: + scenario = await async_client.scenarios.start_run( + scenario_id="scenario_id", + benchmark_run_id="benchmark_run_id", + metadata={"foo": "string"}, + run_name="run_name", + run_profile={ + "env_vars": {"foo": "string"}, + "launch_parameters": { + "after_idle": { + "idle_time_seconds": 0, + "on_idle": "shutdown", + }, + "architecture": "x86_64", + "available_ports": [0], + "custom_cpu_cores": 0, + "custom_disk_size": 0, + "custom_gb_memory": 0, + "keep_alive_time_seconds": 0, + "launch_commands": ["string"], + "network_policy_id": "network_policy_id", + "required_services": ["string"], + "resource_size_request": "X_SMALL", + "user_parameters": { + "uid": 0, + "username": "username", + }, + }, + "mounts": [ + { + "object_id": "object_id", + "object_path": "object_path", + "type": "object_mount", + } + ], + "purpose": "purpose", + "secrets": {"foo": "string"}, + }, + ) + assert_matches_type(ScenarioRunView, scenario, path=["response"]) + + @parametrize + async def test_raw_response_start_run(self, async_client: AsyncRunloop) -> None: + response = await async_client.scenarios.with_raw_response.start_run( + scenario_id="scenario_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + scenario = await response.parse() + assert_matches_type(ScenarioRunView, scenario, path=["response"]) + + @parametrize + async def test_streaming_response_start_run(self, async_client: AsyncRunloop) -> None: + async with async_client.scenarios.with_streaming_response.start_run( + scenario_id="scenario_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + scenario = await response.parse() + assert_matches_type(ScenarioRunView, scenario, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_secrets.py b/tests/api_resources/test_secrets.py new file mode 100644 index 000000000..7f0ff8e21 --- /dev/null +++ b/tests/api_resources/test_secrets.py @@ -0,0 +1,319 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from runloop_api_client import Runloop, AsyncRunloop +from runloop_api_client.types import ( + SecretView, + SecretListView, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestSecrets: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Runloop) -> None: + secret = client.secrets.create( + name="name", + value="value", + ) + assert_matches_type(SecretView, secret, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Runloop) -> None: + response = client.secrets.with_raw_response.create( + name="name", + value="value", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + secret = response.parse() + assert_matches_type(SecretView, secret, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Runloop) -> None: + with client.secrets.with_streaming_response.create( + name="name", + value="value", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + secret = response.parse() + assert_matches_type(SecretView, secret, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_update(self, client: Runloop) -> None: + secret = client.secrets.update( + name="name", + value="value", + ) + assert_matches_type(SecretView, secret, path=["response"]) + + @parametrize + def test_raw_response_update(self, client: Runloop) -> None: + response = client.secrets.with_raw_response.update( + name="name", + value="value", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + secret = response.parse() + assert_matches_type(SecretView, secret, path=["response"]) + + @parametrize + def test_streaming_response_update(self, client: Runloop) -> None: + with client.secrets.with_streaming_response.update( + name="name", + value="value", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + secret = response.parse() + assert_matches_type(SecretView, secret, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_update(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"): + client.secrets.with_raw_response.update( + name="", + value="value", + ) + + @parametrize + def test_method_list(self, client: Runloop) -> None: + secret = client.secrets.list() + assert_matches_type(SecretListView, secret, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Runloop) -> None: + secret = client.secrets.list( + limit=0, + ) + assert_matches_type(SecretListView, secret, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Runloop) -> None: + response = client.secrets.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + secret = response.parse() + assert_matches_type(SecretListView, secret, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Runloop) -> None: + with client.secrets.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + secret = response.parse() + assert_matches_type(SecretListView, secret, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_delete(self, client: Runloop) -> None: + secret = client.secrets.delete( + "name", + ) + assert_matches_type(SecretView, secret, path=["response"]) + + @parametrize + def test_raw_response_delete(self, client: Runloop) -> None: + response = client.secrets.with_raw_response.delete( + "name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + secret = response.parse() + assert_matches_type(SecretView, secret, path=["response"]) + + @parametrize + def test_streaming_response_delete(self, client: Runloop) -> None: + with client.secrets.with_streaming_response.delete( + "name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + secret = response.parse() + assert_matches_type(SecretView, secret, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_delete(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"): + client.secrets.with_raw_response.delete( + "", + ) + + +class TestAsyncSecrets: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_create(self, async_client: AsyncRunloop) -> None: + secret = await async_client.secrets.create( + name="name", + value="value", + ) + assert_matches_type(SecretView, secret, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncRunloop) -> None: + response = await async_client.secrets.with_raw_response.create( + name="name", + value="value", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + secret = await response.parse() + assert_matches_type(SecretView, secret, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncRunloop) -> None: + async with async_client.secrets.with_streaming_response.create( + name="name", + value="value", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + secret = await response.parse() + assert_matches_type(SecretView, secret, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_update(self, async_client: AsyncRunloop) -> None: + secret = await async_client.secrets.update( + name="name", + value="value", + ) + assert_matches_type(SecretView, secret, path=["response"]) + + @parametrize + async def test_raw_response_update(self, async_client: AsyncRunloop) -> None: + response = await async_client.secrets.with_raw_response.update( + name="name", + value="value", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + secret = await response.parse() + assert_matches_type(SecretView, secret, path=["response"]) + + @parametrize + async def test_streaming_response_update(self, async_client: AsyncRunloop) -> None: + async with async_client.secrets.with_streaming_response.update( + name="name", + value="value", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + secret = await response.parse() + assert_matches_type(SecretView, secret, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_update(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"): + await async_client.secrets.with_raw_response.update( + name="", + value="value", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncRunloop) -> None: + secret = await async_client.secrets.list() + assert_matches_type(SecretListView, secret, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncRunloop) -> None: + secret = await async_client.secrets.list( + limit=0, + ) + assert_matches_type(SecretListView, secret, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncRunloop) -> None: + response = await async_client.secrets.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + secret = await response.parse() + assert_matches_type(SecretListView, secret, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncRunloop) -> None: + async with async_client.secrets.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + secret = await response.parse() + assert_matches_type(SecretListView, secret, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_delete(self, async_client: AsyncRunloop) -> None: + secret = await async_client.secrets.delete( + "name", + ) + assert_matches_type(SecretView, secret, path=["response"]) + + @parametrize + async def test_raw_response_delete(self, async_client: AsyncRunloop) -> None: + response = await async_client.secrets.with_raw_response.delete( + "name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + secret = await response.parse() + assert_matches_type(SecretView, secret, path=["response"]) + + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncRunloop) -> None: + async with async_client.secrets.with_streaming_response.delete( + "name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + secret = await response.parse() + assert_matches_type(SecretView, secret, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_delete(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"): + await async_client.secrets.with_raw_response.delete( + "", + ) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..b68f5d7ab --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,84 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +import logging +from typing import TYPE_CHECKING, Iterator, AsyncIterator + +import httpx +import pytest +from pytest_asyncio import is_async_test + +from runloop_api_client import Runloop, AsyncRunloop, DefaultAioHttpClient +from runloop_api_client._utils import is_dict + +if TYPE_CHECKING: + from _pytest.fixtures import FixtureRequest # pyright: ignore[reportPrivateImportUsage] + +pytest.register_assert_rewrite("tests.utils") + +logging.getLogger("runloop_api_client").setLevel(logging.DEBUG) + + +# automatically add `pytest.mark.asyncio()` to all of our async tests +# so we don't have to add that boilerplate everywhere +def pytest_collection_modifyitems(items: list[pytest.Function]) -> None: + pytest_asyncio_tests = (item for item in items if is_async_test(item)) + session_scope_marker = pytest.mark.asyncio(loop_scope="session") + for async_test in pytest_asyncio_tests: + async_test.add_marker(session_scope_marker, append=False) + + # We skip tests that use both the aiohttp client and respx_mock as respx_mock + # doesn't support custom transports. + for item in items: + if "async_client" not in item.fixturenames or "respx_mock" not in item.fixturenames: + continue + + if not hasattr(item, "callspec"): + continue + + async_client_param = item.callspec.params.get("async_client") + if is_dict(async_client_param) and async_client_param.get("http_client") == "aiohttp": + item.add_marker(pytest.mark.skip(reason="aiohttp client is not compatible with respx_mock")) + + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + +bearer_token = "My Bearer Token" + + +@pytest.fixture(scope="session") +def client(request: FixtureRequest) -> Iterator[Runloop]: + strict = getattr(request, "param", True) + if not isinstance(strict, bool): + raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") + + with Runloop(base_url=base_url, bearer_token=bearer_token, _strict_response_validation=strict) as client: + yield client + + +@pytest.fixture(scope="session") +async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncRunloop]: + 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 AsyncRunloop( + base_url=base_url, bearer_token=bearer_token, _strict_response_validation=strict, http_client=http_client + ) as client: + yield client diff --git a/tests/sample_file.txt b/tests/sample_file.txt new file mode 100644 index 000000000..af5626b4a --- /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 000000000..725862258 --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,2016 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import gc +import os +import sys +import json +import asyncio +import inspect +import dataclasses +import tracemalloc +from typing import Any, Union, TypeVar, Callable, Iterable, Iterator, Optional, Coroutine, cast +from unittest import mock +from typing_extensions import Literal, AsyncIterator, override + +import httpx +import pytest +from respx import MockRouter +from pydantic import ValidationError + +from runloop_api_client import Runloop, AsyncRunloop, APIResponseValidationError +from runloop_api_client._types import Omit +from runloop_api_client._utils import asyncify +from runloop_api_client._models import BaseModel, FinalRequestOptions +from runloop_api_client._exceptions import RunloopError, APIStatusError, APITimeoutError, APIResponseValidationError +from runloop_api_client._base_client import ( + DEFAULT_TIMEOUT, + HTTPX_DEFAULT_TIMEOUT, + BaseClient, + OtherPlatform, + DefaultHttpxClient, + DefaultAsyncHttpxClient, + get_platform, + make_request_options, +) + +from .utils import update_env + +T = TypeVar("T") +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") +bearer_token = "My Bearer Token" + + +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 mirror_request_content(request: httpx.Request) -> httpx.Response: + return httpx.Response(200, content=request.content) + + +# note: we can't use the httpx.MockTransport class as it consumes the request +# body itself, which means we can't test that the body is read lazily +class MockTransport(httpx.BaseTransport, httpx.AsyncBaseTransport): + def __init__( + self, + handler: Callable[[httpx.Request], httpx.Response] + | Callable[[httpx.Request], Coroutine[Any, Any, httpx.Response]], + ) -> None: + self.handler = handler + + @override + def handle_request( + self, + request: httpx.Request, + ) -> httpx.Response: + assert not inspect.iscoroutinefunction(self.handler), "handler must not be a coroutine function" + assert inspect.isfunction(self.handler), "handler must be a function" + return self.handler(request) + + @override + async def handle_async_request( + self, + request: httpx.Request, + ) -> httpx.Response: + assert inspect.iscoroutinefunction(self.handler), "handler must be a coroutine function" + return await self.handler(request) + + +@dataclasses.dataclass +class Counter: + value: int = 0 + + +def _make_sync_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> Iterator[T]: + for item in iterable: + if counter: + counter.value += 1 + yield item + + +async def _make_async_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> AsyncIterator[T]: + for item in iterable: + if counter: + counter.value += 1 + yield item + + +def _get_open_connections(client: Runloop | AsyncRunloop) -> int: + transport = client._client._transport + assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) + + pool = transport._pool + return len(pool._requests) + + +class TestRunloop: + @pytest.mark.respx(base_url=base_url) + def test_raw_response(self, respx_mock: MockRouter, client: Runloop) -> None: + respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = client.post("/foo", cast_to=httpx.Response) + assert response.status_code == 200 + assert isinstance(response, httpx.Response) + assert response.json() == {"foo": "bar"} + + @pytest.mark.respx(base_url=base_url) + def test_raw_response_for_binary(self, respx_mock: MockRouter, client: Runloop) -> None: + respx_mock.post("/foo").mock( + return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}') + ) + + response = client.post("/foo", cast_to=httpx.Response) + assert response.status_code == 200 + assert isinstance(response, httpx.Response) + assert response.json() == {"foo": "bar"} + + def test_copy(self, client: Runloop) -> None: + copied = client.copy() + assert id(copied) != id(client) + + copied = client.copy(bearer_token="another My Bearer Token") + assert copied.bearer_token == "another My Bearer Token" + assert client.bearer_token == "My Bearer Token" + + def test_copy_default_options(self, client: Runloop) -> None: + # options that have a default are overridden correctly + copied = client.copy(max_retries=7) + assert copied.max_retries == 7 + assert client.max_retries == 5 + + copied2 = copied.copy(max_retries=6) + assert copied2.max_retries == 6 + assert copied.max_retries == 7 + + # timeout + assert isinstance(client.timeout, httpx.Timeout) + copied = client.copy(timeout=None) + assert copied.timeout is None + assert isinstance(client.timeout, httpx.Timeout) + + def test_copy_default_headers(self) -> None: + client = Runloop( + base_url=base_url, + bearer_token=bearer_token, + _strict_response_validation=True, + default_headers={"X-Foo": "bar"}, + ) + assert client.default_headers["X-Foo"] == "bar" + + # does not override the already given value when not specified + copied = client.copy() + assert copied.default_headers["X-Foo"] == "bar" + + # merges already given headers + copied = client.copy(default_headers={"X-Bar": "stainless"}) + assert copied.default_headers["X-Foo"] == "bar" + assert copied.default_headers["X-Bar"] == "stainless" + + # uses new values for any already given headers + copied = client.copy(default_headers={"X-Foo": "stainless"}) + assert copied.default_headers["X-Foo"] == "stainless" + + # set_default_headers + + # completely overrides already set values + copied = client.copy(set_default_headers={}) + assert copied.default_headers.get("X-Foo") is None + + copied = client.copy(set_default_headers={"X-Bar": "Robert"}) + assert copied.default_headers["X-Bar"] == "Robert" + + with pytest.raises( + ValueError, + match="`default_headers` and `set_default_headers` arguments are mutually exclusive", + ): + client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"}) + client.close() + + def test_copy_default_query(self) -> None: + client = Runloop( + base_url=base_url, bearer_token=bearer_token, _strict_response_validation=True, default_query={"foo": "bar"} + ) + assert _get_params(client)["foo"] == "bar" + + # does not override the already given value when not specified + copied = client.copy() + assert _get_params(copied)["foo"] == "bar" + + # merges already given params + copied = client.copy(default_query={"bar": "stainless"}) + params = _get_params(copied) + assert params["foo"] == "bar" + assert params["bar"] == "stainless" + + # uses new values for any already given headers + copied = client.copy(default_query={"foo": "stainless"}) + assert _get_params(copied)["foo"] == "stainless" + + # set_default_query + + # completely overrides already set values + copied = client.copy(set_default_query={}) + assert _get_params(copied) == {} + + copied = client.copy(set_default_query={"bar": "Robert"}) + assert _get_params(copied)["bar"] == "Robert" + + with pytest.raises( + ValueError, + # TODO: update + match="`default_query` and `set_default_query` arguments are mutually exclusive", + ): + client.copy(set_default_query={}, default_query={"foo": "Bar"}) + + client.close() + + def test_copy_signature(self, client: Runloop) -> None: + # ensure the same parameters that can be passed to the client are defined in the `.copy()` method + init_signature = inspect.signature( + # mypy doesn't like that we access the `__init__` property. + client.__init__, # type: ignore[misc] + ) + copy_signature = inspect.signature(client.copy) + exclude_params = {"transport", "proxies", "_strict_response_validation"} + + for name in init_signature.parameters.keys(): + if name in exclude_params: + continue + + copy_param = copy_signature.parameters.get(name) + assert copy_param is not None, f"copy() signature is missing the {name} param" + + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") + def test_copy_build_request(self, client: Runloop) -> None: + options = FinalRequestOptions(method="get", url="/foo") + + def build_request(options: FinalRequestOptions) -> None: + client_copy = client.copy() + client_copy._build_request(options) + + # ensure that the machinery is warmed up before tracing starts. + build_request(options) + gc.collect() + + tracemalloc.start(1000) + + snapshot_before = tracemalloc.take_snapshot() + + ITERATIONS = 10 + for _ in range(ITERATIONS): + build_request(options) + + gc.collect() + snapshot_after = tracemalloc.take_snapshot() + + tracemalloc.stop() + + def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.StatisticDiff) -> None: + if diff.count == 0: + # Avoid false positives by considering only leaks (i.e. allocations that persist). + return + + if diff.count % ITERATIONS != 0: + # Avoid false positives by considering only leaks that appear per iteration. + return + + for frame in diff.traceback: + if any( + frame.filename.endswith(fragment) + for fragment in [ + # to_raw_response_wrapper leaks through the @functools.wraps() decorator. + # + # removing the decorator fixes the leak for reasons we don't understand. + "runloop_api_client/_legacy_response.py", + "runloop_api_client/_response.py", + # pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason. + "runloop_api_client/_compat.py", + # Standard library leaks we don't care about. + "/logging/__init__.py", + ] + ): + return + + leaks.append(diff) + + leaks: list[tracemalloc.StatisticDiff] = [] + for diff in snapshot_after.compare_to(snapshot_before, "traceback"): + add_leak(leaks, diff) + if leaks: + for leak in leaks: + print("MEMORY LEAK:", leak) + for frame in leak.traceback: + print(frame) + raise AssertionError() + + def test_request_timeout(self, client: Runloop) -> None: + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == DEFAULT_TIMEOUT + + request = client._build_request(FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0))) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == httpx.Timeout(100.0) + + def test_client_timeout_option(self) -> None: + client = Runloop( + base_url=base_url, bearer_token=bearer_token, _strict_response_validation=True, timeout=httpx.Timeout(0) + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == httpx.Timeout(0) + + client.close() + + def test_http_client_timeout_option(self) -> None: + # custom timeout given to the httpx client should be used + with httpx.Client(timeout=None) as http_client: + client = Runloop( + base_url=base_url, bearer_token=bearer_token, _strict_response_validation=True, http_client=http_client + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == httpx.Timeout(None) + + client.close() + + # no timeout given to the httpx client should not use the httpx default + with httpx.Client() as http_client: + client = Runloop( + base_url=base_url, bearer_token=bearer_token, _strict_response_validation=True, http_client=http_client + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == DEFAULT_TIMEOUT + + client.close() + + # explicitly passing the default timeout currently results in it being ignored + with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: + client = Runloop( + base_url=base_url, bearer_token=bearer_token, _strict_response_validation=True, http_client=http_client + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == DEFAULT_TIMEOUT # our default + + client.close() + + async def test_invalid_http_client(self) -> None: + with pytest.raises(TypeError, match="Invalid `http_client` arg"): + async with httpx.AsyncClient() as http_client: + Runloop( + base_url=base_url, + bearer_token=bearer_token, + _strict_response_validation=True, + http_client=cast(Any, http_client), + ) + + def test_default_headers_option(self) -> None: + test_client = Runloop( + base_url=base_url, + bearer_token=bearer_token, + _strict_response_validation=True, + default_headers={"X-Foo": "bar"}, + ) + request = test_client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("x-foo") == "bar" + assert request.headers.get("x-stainless-lang") == "python" + + test_client2 = Runloop( + base_url=base_url, + bearer_token=bearer_token, + _strict_response_validation=True, + default_headers={ + "X-Foo": "stainless", + "X-Stainless-Lang": "my-overriding-header", + }, + ) + request = test_client2._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("x-foo") == "stainless" + assert request.headers.get("x-stainless-lang") == "my-overriding-header" + + test_client.close() + test_client2.close() + + def test_validate_headers(self) -> None: + client = Runloop(base_url=base_url, bearer_token=bearer_token, _strict_response_validation=True) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("Authorization") == f"Bearer {bearer_token}" + + with pytest.raises(RunloopError): + with update_env(**{"RUNLOOP_API_KEY": Omit()}): + client2 = Runloop(base_url=base_url, bearer_token=None, _strict_response_validation=True) + _ = client2 + + def test_default_query_option(self) -> None: + client = Runloop( + base_url=base_url, + bearer_token=bearer_token, + _strict_response_validation=True, + default_query={"query_param": "bar"}, + ) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + url = httpx.URL(request.url) + assert dict(url.params) == {"query_param": "bar"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/foo", + params={"foo": "baz", "query_param": "overridden"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} + + client.close() + + def test_request_extra_json(self, client: Runloop) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + extra_json={"baz": False}, + ), + ) + data = json.loads(request.content.decode("utf-8")) + assert data == {"foo": "bar", "baz": False} + + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + extra_json={"baz": False}, + ), + ) + data = json.loads(request.content.decode("utf-8")) + assert data == {"baz": False} + + # `extra_json` takes priority over `json_data` when keys clash + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar", "baz": True}, + extra_json={"baz": None}, + ), + ) + data = json.loads(request.content.decode("utf-8")) + assert data == {"foo": "bar", "baz": None} + + def test_request_extra_headers(self, client: Runloop) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options(extra_headers={"X-Foo": "Foo"}), + ), + ) + assert request.headers.get("X-Foo") == "Foo" + + # `extra_headers` takes priority over `default_headers` when keys clash + request = client.with_options(default_headers={"X-Bar": "true"})._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + extra_headers={"X-Bar": "false"}, + ), + ), + ) + assert request.headers.get("X-Bar") == "false" + + def test_request_extra_query(self, client: Runloop) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + extra_query={"my_query_param": "Foo"}, + ), + ), + ) + params = dict(request.url.params) + assert params == {"my_query_param": "Foo"} + + # if both `query` and `extra_query` are given, they are merged + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + query={"bar": "1"}, + extra_query={"foo": "2"}, + ), + ), + ) + params = dict(request.url.params) + assert params == {"bar": "1", "foo": "2"} + + # `extra_query` takes priority over `query` when keys clash + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + query={"foo": "1"}, + extra_query={"foo": "2"}, + ), + ), + ) + params = dict(request.url.params) + assert params == {"foo": "2"} + + def test_multipart_repeating_array(self, client: Runloop) -> None: + request = client._build_request( + FinalRequestOptions.construct( + method="post", + url="/foo", + headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"}, + json_data={"array": ["foo", "bar"]}, + files=[("foo.txt", b"hello world")], + ) + ) + + assert request.read().split(b"\r\n") == [ + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="array[]"', + b"", + b"foo", + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="array[]"', + b"", + b"bar", + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="foo.txt"; filename="upload"', + b"Content-Type: application/octet-stream", + b"", + b"hello world", + b"--6b7ba517decee4a450543ea6ae821c82--", + b"", + ] + + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload(self, respx_mock: MockRouter, client: Runloop) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + response = client.post( + "/upload", + content=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + def test_binary_content_upload_with_iterator(self) -> None: + file_content = b"Hello, this is a test file." + counter = Counter() + iterator = _make_sync_iterator([file_content], counter=counter) + + def mock_handler(request: httpx.Request) -> httpx.Response: + assert counter.value == 0, "the request body should not have been read" + return httpx.Response(200, content=request.read()) + + with Runloop( + base_url=base_url, + bearer_token=bearer_token, + _strict_response_validation=True, + http_client=httpx.Client(transport=MockTransport(handler=mock_handler)), + ) as client: + response = client.post( + "/upload", + content=iterator, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + assert counter.value == 1 + + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload_with_body_is_deprecated(self, respx_mock: MockRouter, client: Runloop) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + with pytest.deprecated_call( + match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead." + ): + response = client.post( + "/upload", + body=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + @pytest.mark.respx(base_url=base_url) + def test_basic_union_response(self, respx_mock: MockRouter, client: Runloop) -> None: + class Model1(BaseModel): + name: str + + class Model2(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model2) + assert response.foo == "bar" + + @pytest.mark.respx(base_url=base_url) + def test_union_response_different_types(self, respx_mock: MockRouter, client: Runloop) -> None: + """Union of objects with the same field name using a different type""" + + class Model1(BaseModel): + foo: int + + class Model2(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model2) + assert response.foo == "bar" + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1})) + + response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model1) + assert response.foo == 1 + + @pytest.mark.respx(base_url=base_url) + def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter, client: Runloop) -> None: + """ + Response that sets Content-Type to something other than application/json but returns json data + """ + + class Model(BaseModel): + foo: int + + respx_mock.get("/foo").mock( + return_value=httpx.Response( + 200, + content=json.dumps({"foo": 2}), + headers={"Content-Type": "application/text"}, + ) + ) + + response = client.get("/foo", cast_to=Model) + assert isinstance(response, Model) + assert response.foo == 2 + + @pytest.mark.respx(base_url=base_url) + def test_idempotency_header_options(self, respx_mock: MockRouter, client: Runloop) -> None: + respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={})) + + response = client.post("/foo", cast_to=httpx.Response) + + header = response.request.headers.get("x-request-id") + assert header is not None + assert header.startswith("stainless-python-retry") + + # explicit header + response = client.post( + "/foo", cast_to=httpx.Response, options=make_request_options(extra_headers={"x-request-id": "custom-key"}) + ) + assert response.request.headers.get("x-request-id") == "custom-key" + + response = client.post( + "/foo", cast_to=httpx.Response, options=make_request_options(extra_headers={"x-request-id": "custom-key"}) + ) + assert response.request.headers.get("x-request-id") == "custom-key" + + # custom argument + response = client.post( + "/foo", cast_to=httpx.Response, options=make_request_options(idempotency_key="custom-key") + ) + assert response.request.headers.get("x-request-id") == "custom-key" + + def test_base_url_setter(self) -> None: + client = Runloop( + base_url="https://example.com/from_init", bearer_token=bearer_token, _strict_response_validation=True + ) + assert client.base_url == "https://example.com/from_init/" + + client.base_url = "https://example.com/from_setter" # type: ignore[assignment] + + assert client.base_url == "https://example.com/from_setter/" + + client.close() + + def test_base_url_env(self) -> None: + with update_env(RUNLOOP_BASE_URL="http://localhost:5000/from/env"): + client = Runloop(bearer_token=bearer_token, _strict_response_validation=True) + assert client.base_url == "http://localhost:5000/from/env/" + + @pytest.mark.parametrize( + "client", + [ + Runloop( + base_url="http://localhost:5000/custom/path/", + bearer_token=bearer_token, + _strict_response_validation=True, + ), + Runloop( + base_url="http://localhost:5000/custom/path/", + bearer_token=bearer_token, + _strict_response_validation=True, + http_client=httpx.Client(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_base_url_trailing_slash(self, client: Runloop) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "http://localhost:5000/custom/path/foo" + client.close() + + @pytest.mark.parametrize( + "client", + [ + Runloop( + base_url="http://localhost:5000/custom/path/", + bearer_token=bearer_token, + _strict_response_validation=True, + ), + Runloop( + base_url="http://localhost:5000/custom/path/", + bearer_token=bearer_token, + _strict_response_validation=True, + http_client=httpx.Client(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_base_url_no_trailing_slash(self, client: Runloop) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "http://localhost:5000/custom/path/foo" + client.close() + + @pytest.mark.parametrize( + "client", + [ + Runloop( + base_url="http://localhost:5000/custom/path/", + bearer_token=bearer_token, + _strict_response_validation=True, + ), + Runloop( + base_url="http://localhost:5000/custom/path/", + bearer_token=bearer_token, + _strict_response_validation=True, + http_client=httpx.Client(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_absolute_request_url(self, client: Runloop) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="https://myapi.com/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "https://myapi.com/foo" + client.close() + + def test_copied_client_does_not_close_http(self) -> None: + test_client = Runloop(base_url=base_url, bearer_token=bearer_token, _strict_response_validation=True) + assert not test_client.is_closed() + + copied = test_client.copy() + assert copied is not test_client + + del copied + + assert not test_client.is_closed() + + def test_client_context_manager(self) -> None: + test_client = Runloop(base_url=base_url, bearer_token=bearer_token, _strict_response_validation=True) + with test_client as c2: + assert c2 is test_client + assert not c2.is_closed() + assert not test_client.is_closed() + assert test_client.is_closed() + + @pytest.mark.respx(base_url=base_url) + def test_client_response_validation_error(self, respx_mock: MockRouter, client: Runloop) -> None: + class Model(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}})) + + with pytest.raises(APIResponseValidationError) as exc: + client.get("/foo", cast_to=Model) + + assert isinstance(exc.value.__cause__, ValidationError) + + def test_client_max_retries_validation(self) -> None: + with pytest.raises(TypeError, match=r"max_retries cannot be None"): + Runloop( + base_url=base_url, + bearer_token=bearer_token, + _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 = Runloop(base_url=base_url, bearer_token=bearer_token, _strict_response_validation=True) + + with pytest.raises(APIResponseValidationError): + strict_client.get("/foo", cast_to=Model) + + non_strict_client = Runloop(base_url=base_url, bearer_token=bearer_token, _strict_response_validation=False) + + response = non_strict_client.get("/foo", cast_to=Model) + assert isinstance(response, str) # type: ignore[unreachable] + + strict_client.close() + non_strict_client.close() + + @pytest.mark.parametrize( + "remaining_retries,retry_after,timeout", + [ + [3, "20", 20], + [3, "0", 1], + [3, "-10", 1], + [3, "60", 60], + [3, "61", 1], + [3, "Fri, 29 Sep 2023 16:26:57 GMT", 20], + [3, "Fri, 29 Sep 2023 16:26:37 GMT", 1], + [3, "Fri, 29 Sep 2023 16:26:27 GMT", 1], + [3, "Fri, 29 Sep 2023 16:27:37 GMT", 60], + [3, "Fri, 29 Sep 2023 16:27:38 GMT", 1], + [3, "99999999999999999999999999999999999", 1], + [3, "Zun, 29 Sep 2023 16:26:27 GMT", 1], + [3, "", 1], + [2, "", 1 * 2.0], + [1, "", 1 * 4.0], + [-1100, "", 60], # test large number potentially overflowing + ], + ) + @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) + def test_parse_retry_after_header( + self, remaining_retries: int, retry_after: str, timeout: float, client: Runloop + ) -> None: + headers = httpx.Headers({"retry-after": retry_after}) + options = FinalRequestOptions(method="get", url="/foo", max_retries=3) + calculated = client._calculate_retry_timeout(remaining_retries, options, headers) + assert calculated == pytest.approx(timeout, 1 * 0.875) # pyright: ignore[reportUnknownMemberType] + + @mock.patch("runloop_api_client._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: Runloop) -> None: + respx_mock.post("/v1/devboxes").mock(side_effect=httpx.TimeoutException("Test timeout error")) + + with pytest.raises(APITimeoutError): + client.devboxes.with_streaming_response.create().__enter__() + + assert _get_open_connections(client) == 0 + + @mock.patch("runloop_api_client._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: Runloop) -> None: + respx_mock.post("/v1/devboxes").mock(return_value=httpx.Response(500)) + + with pytest.raises(APIStatusError): + client.devboxes.with_streaming_response.create().__enter__() + assert _get_open_connections(client) == 0 + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("runloop_api_client._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: Runloop, + failures_before_success: int, + failure_mode: Literal["status", "exception"], + respx_mock: MockRouter, + ) -> None: + client = client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + if failure_mode == "exception": + raise RuntimeError("oops") + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.post("/v1/devboxes").mock(side_effect=retry_handler) + + response = client.devboxes.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 + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("runloop_api_client._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + def test_omit_retry_count_header( + self, client: Runloop, failures_before_success: int, respx_mock: MockRouter + ) -> None: + client = client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.post("/v1/devboxes").mock(side_effect=retry_handler) + + response = client.devboxes.with_raw_response.create(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("runloop_api_client._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + def test_overwrite_retry_count_header( + self, client: Runloop, failures_before_success: int, respx_mock: MockRouter + ) -> None: + client = client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.post("/v1/devboxes").mock(side_effect=retry_handler) + + response = client.devboxes.with_raw_response.create(extra_headers={"x-stainless-retry-count": "42"}) + + assert response.http_request.headers.get("x-stainless-retry-count") == "42" + + def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: + # Test that the proxy environment variables are set correctly + monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + # Delete in case our environment has any proxy env vars set + monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.delenv("NO_PROXY", raising=False) + monkeypatch.delenv("http_proxy", raising=False) + monkeypatch.delenv("https_proxy", raising=False) + monkeypatch.delenv("all_proxy", raising=False) + monkeypatch.delenv("no_proxy", raising=False) + + client = DefaultHttpxClient() + + mounts = tuple(client._mounts.items()) + assert len(mounts) == 1 + assert mounts[0][0].pattern == "https://" + + @pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning") + def test_default_client_creation(self) -> None: + # Ensure that the client can be initialized without any exceptions + DefaultHttpxClient( + verify=True, + cert=None, + trust_env=True, + http1=True, + http2=False, + limits=httpx.Limits(max_connections=100, max_keepalive_connections=20), + ) + + @pytest.mark.respx(base_url=base_url) + def test_follow_redirects(self, respx_mock: MockRouter, client: Runloop) -> None: + # Test that the default follow_redirects=True allows following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"})) + + response = client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) + assert response.status_code == 200 + assert response.json() == {"status": "ok"} + + @pytest.mark.respx(base_url=base_url) + def test_follow_redirects_disabled(self, respx_mock: MockRouter, client: Runloop) -> None: + # Test that follow_redirects=False prevents following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + + with pytest.raises(APIStatusError) as exc_info: + client.post("/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response) + + assert exc_info.value.response.status_code == 302 + assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected" + + +class TestAsyncRunloop: + @pytest.mark.respx(base_url=base_url) + async def test_raw_response(self, respx_mock: MockRouter, async_client: AsyncRunloop) -> None: + respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = await async_client.post("/foo", cast_to=httpx.Response) + assert response.status_code == 200 + assert isinstance(response, httpx.Response) + assert response.json() == {"foo": "bar"} + + @pytest.mark.respx(base_url=base_url) + async def test_raw_response_for_binary(self, respx_mock: MockRouter, async_client: AsyncRunloop) -> None: + respx_mock.post("/foo").mock( + return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}') + ) + + response = await async_client.post("/foo", cast_to=httpx.Response) + assert response.status_code == 200 + assert isinstance(response, httpx.Response) + assert response.json() == {"foo": "bar"} + + def test_copy(self, async_client: AsyncRunloop) -> None: + copied = async_client.copy() + assert id(copied) != id(async_client) + + copied = async_client.copy(bearer_token="another My Bearer Token") + assert copied.bearer_token == "another My Bearer Token" + assert async_client.bearer_token == "My Bearer Token" + + def test_copy_default_options(self, async_client: AsyncRunloop) -> None: + # options that have a default are overridden correctly + copied = async_client.copy(max_retries=7) + assert copied.max_retries == 7 + assert async_client.max_retries == 5 + + copied2 = copied.copy(max_retries=6) + assert copied2.max_retries == 6 + assert copied.max_retries == 7 + + # timeout + assert isinstance(async_client.timeout, httpx.Timeout) + copied = async_client.copy(timeout=None) + assert copied.timeout is None + assert isinstance(async_client.timeout, httpx.Timeout) + + async def test_copy_default_headers(self) -> None: + client = AsyncRunloop( + base_url=base_url, + bearer_token=bearer_token, + _strict_response_validation=True, + default_headers={"X-Foo": "bar"}, + ) + assert client.default_headers["X-Foo"] == "bar" + + # does not override the already given value when not specified + copied = client.copy() + assert copied.default_headers["X-Foo"] == "bar" + + # merges already given headers + copied = client.copy(default_headers={"X-Bar": "stainless"}) + assert copied.default_headers["X-Foo"] == "bar" + assert copied.default_headers["X-Bar"] == "stainless" + + # uses new values for any already given headers + copied = client.copy(default_headers={"X-Foo": "stainless"}) + assert copied.default_headers["X-Foo"] == "stainless" + + # set_default_headers + + # completely overrides already set values + copied = client.copy(set_default_headers={}) + assert copied.default_headers.get("X-Foo") is None + + copied = client.copy(set_default_headers={"X-Bar": "Robert"}) + assert copied.default_headers["X-Bar"] == "Robert" + + with pytest.raises( + ValueError, + match="`default_headers` and `set_default_headers` arguments are mutually exclusive", + ): + client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"}) + await client.close() + + async def test_copy_default_query(self) -> None: + client = AsyncRunloop( + base_url=base_url, bearer_token=bearer_token, _strict_response_validation=True, default_query={"foo": "bar"} + ) + assert _get_params(client)["foo"] == "bar" + + # does not override the already given value when not specified + copied = client.copy() + assert _get_params(copied)["foo"] == "bar" + + # merges already given params + copied = client.copy(default_query={"bar": "stainless"}) + params = _get_params(copied) + assert params["foo"] == "bar" + assert params["bar"] == "stainless" + + # uses new values for any already given headers + copied = client.copy(default_query={"foo": "stainless"}) + assert _get_params(copied)["foo"] == "stainless" + + # set_default_query + + # completely overrides already set values + copied = client.copy(set_default_query={}) + assert _get_params(copied) == {} + + copied = client.copy(set_default_query={"bar": "Robert"}) + assert _get_params(copied)["bar"] == "Robert" + + with pytest.raises( + ValueError, + # TODO: update + match="`default_query` and `set_default_query` arguments are mutually exclusive", + ): + client.copy(set_default_query={}, default_query={"foo": "Bar"}) + + await client.close() + + def test_copy_signature(self, async_client: AsyncRunloop) -> None: + # ensure the same parameters that can be passed to the client are defined in the `.copy()` method + init_signature = inspect.signature( + # mypy doesn't like that we access the `__init__` property. + async_client.__init__, # type: ignore[misc] + ) + copy_signature = inspect.signature(async_client.copy) + exclude_params = {"transport", "proxies", "_strict_response_validation"} + + for name in init_signature.parameters.keys(): + if name in exclude_params: + continue + + copy_param = copy_signature.parameters.get(name) + assert copy_param is not None, f"copy() signature is missing the {name} param" + + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") + def test_copy_build_request(self, async_client: AsyncRunloop) -> None: + options = FinalRequestOptions(method="get", url="/foo") + + def build_request(options: FinalRequestOptions) -> None: + client_copy = async_client.copy() + client_copy._build_request(options) + + # ensure that the machinery is warmed up before tracing starts. + build_request(options) + gc.collect() + + tracemalloc.start(1000) + + snapshot_before = tracemalloc.take_snapshot() + + ITERATIONS = 10 + for _ in range(ITERATIONS): + build_request(options) + + gc.collect() + snapshot_after = tracemalloc.take_snapshot() + + tracemalloc.stop() + + def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.StatisticDiff) -> None: + if diff.count == 0: + # Avoid false positives by considering only leaks (i.e. allocations that persist). + return + + if diff.count % ITERATIONS != 0: + # Avoid false positives by considering only leaks that appear per iteration. + return + + for frame in diff.traceback: + if any( + frame.filename.endswith(fragment) + for fragment in [ + # to_raw_response_wrapper leaks through the @functools.wraps() decorator. + # + # removing the decorator fixes the leak for reasons we don't understand. + "runloop_api_client/_legacy_response.py", + "runloop_api_client/_response.py", + # pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason. + "runloop_api_client/_compat.py", + # Standard library leaks we don't care about. + "/logging/__init__.py", + ] + ): + return + + leaks.append(diff) + + leaks: list[tracemalloc.StatisticDiff] = [] + for diff in snapshot_after.compare_to(snapshot_before, "traceback"): + add_leak(leaks, diff) + if leaks: + for leak in leaks: + print("MEMORY LEAK:", leak) + for frame in leak.traceback: + print(frame) + raise AssertionError() + + async def test_request_timeout(self, async_client: AsyncRunloop) -> None: + request = async_client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == DEFAULT_TIMEOUT + + request = async_client._build_request( + FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0)) + ) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == httpx.Timeout(100.0) + + async def test_client_timeout_option(self) -> None: + client = AsyncRunloop( + base_url=base_url, bearer_token=bearer_token, _strict_response_validation=True, timeout=httpx.Timeout(0) + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == httpx.Timeout(0) + + await client.close() + + async def test_http_client_timeout_option(self) -> None: + # custom timeout given to the httpx client should be used + async with httpx.AsyncClient(timeout=None) as http_client: + client = AsyncRunloop( + base_url=base_url, bearer_token=bearer_token, _strict_response_validation=True, http_client=http_client + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == httpx.Timeout(None) + + await client.close() + + # no timeout given to the httpx client should not use the httpx default + async with httpx.AsyncClient() as http_client: + client = AsyncRunloop( + base_url=base_url, bearer_token=bearer_token, _strict_response_validation=True, http_client=http_client + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == DEFAULT_TIMEOUT + + await client.close() + + # explicitly passing the default timeout currently results in it being ignored + async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: + client = AsyncRunloop( + base_url=base_url, bearer_token=bearer_token, _strict_response_validation=True, http_client=http_client + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == DEFAULT_TIMEOUT # our default + + await client.close() + + def test_invalid_http_client(self) -> None: + with pytest.raises(TypeError, match="Invalid `http_client` arg"): + with httpx.Client() as http_client: + AsyncRunloop( + base_url=base_url, + bearer_token=bearer_token, + _strict_response_validation=True, + http_client=cast(Any, http_client), + ) + + async def test_default_headers_option(self) -> None: + test_client = AsyncRunloop( + base_url=base_url, + bearer_token=bearer_token, + _strict_response_validation=True, + default_headers={"X-Foo": "bar"}, + ) + request = test_client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("x-foo") == "bar" + assert request.headers.get("x-stainless-lang") == "python" + + test_client2 = AsyncRunloop( + base_url=base_url, + bearer_token=bearer_token, + _strict_response_validation=True, + default_headers={ + "X-Foo": "stainless", + "X-Stainless-Lang": "my-overriding-header", + }, + ) + request = test_client2._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("x-foo") == "stainless" + assert request.headers.get("x-stainless-lang") == "my-overriding-header" + + await test_client.close() + await test_client2.close() + + def test_validate_headers(self) -> None: + client = AsyncRunloop(base_url=base_url, bearer_token=bearer_token, _strict_response_validation=True) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("Authorization") == f"Bearer {bearer_token}" + + with pytest.raises(RunloopError): + with update_env(**{"RUNLOOP_API_KEY": Omit()}): + client2 = AsyncRunloop(base_url=base_url, bearer_token=None, _strict_response_validation=True) + _ = client2 + + async def test_default_query_option(self) -> None: + client = AsyncRunloop( + base_url=base_url, + bearer_token=bearer_token, + _strict_response_validation=True, + default_query={"query_param": "bar"}, + ) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + url = httpx.URL(request.url) + assert dict(url.params) == {"query_param": "bar"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/foo", + params={"foo": "baz", "query_param": "overridden"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} + + await client.close() + + def test_request_extra_json(self, client: Runloop) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + extra_json={"baz": False}, + ), + ) + data = json.loads(request.content.decode("utf-8")) + assert data == {"foo": "bar", "baz": False} + + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + extra_json={"baz": False}, + ), + ) + data = json.loads(request.content.decode("utf-8")) + assert data == {"baz": False} + + # `extra_json` takes priority over `json_data` when keys clash + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar", "baz": True}, + extra_json={"baz": None}, + ), + ) + data = json.loads(request.content.decode("utf-8")) + assert data == {"foo": "bar", "baz": None} + + def test_request_extra_headers(self, client: Runloop) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options(extra_headers={"X-Foo": "Foo"}), + ), + ) + assert request.headers.get("X-Foo") == "Foo" + + # `extra_headers` takes priority over `default_headers` when keys clash + request = client.with_options(default_headers={"X-Bar": "true"})._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + extra_headers={"X-Bar": "false"}, + ), + ), + ) + assert request.headers.get("X-Bar") == "false" + + def test_request_extra_query(self, client: Runloop) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + extra_query={"my_query_param": "Foo"}, + ), + ), + ) + params = dict(request.url.params) + assert params == {"my_query_param": "Foo"} + + # if both `query` and `extra_query` are given, they are merged + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + query={"bar": "1"}, + extra_query={"foo": "2"}, + ), + ), + ) + params = dict(request.url.params) + assert params == {"bar": "1", "foo": "2"} + + # `extra_query` takes priority over `query` when keys clash + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + query={"foo": "1"}, + extra_query={"foo": "2"}, + ), + ), + ) + params = dict(request.url.params) + assert params == {"foo": "2"} + + def test_multipart_repeating_array(self, async_client: AsyncRunloop) -> None: + request = async_client._build_request( + FinalRequestOptions.construct( + method="post", + url="/foo", + headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"}, + json_data={"array": ["foo", "bar"]}, + files=[("foo.txt", b"hello world")], + ) + ) + + assert request.read().split(b"\r\n") == [ + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="array[]"', + b"", + b"foo", + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="array[]"', + b"", + b"bar", + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="foo.txt"; filename="upload"', + b"Content-Type: application/octet-stream", + b"", + b"hello world", + b"--6b7ba517decee4a450543ea6ae821c82--", + b"", + ] + + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload(self, respx_mock: MockRouter, async_client: AsyncRunloop) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + response = await async_client.post( + "/upload", + content=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + async def test_binary_content_upload_with_asynciterator(self) -> None: + file_content = b"Hello, this is a test file." + counter = Counter() + iterator = _make_async_iterator([file_content], counter=counter) + + async def mock_handler(request: httpx.Request) -> httpx.Response: + assert counter.value == 0, "the request body should not have been read" + return httpx.Response(200, content=await request.aread()) + + async with AsyncRunloop( + base_url=base_url, + bearer_token=bearer_token, + _strict_response_validation=True, + http_client=httpx.AsyncClient(transport=MockTransport(handler=mock_handler)), + ) as client: + response = await client.post( + "/upload", + content=iterator, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + assert counter.value == 1 + + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload_with_body_is_deprecated( + self, respx_mock: MockRouter, async_client: AsyncRunloop + ) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + with pytest.deprecated_call( + match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead." + ): + response = await async_client.post( + "/upload", + body=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + @pytest.mark.respx(base_url=base_url) + async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncRunloop) -> None: + class Model1(BaseModel): + name: str + + class Model2(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model2) + assert response.foo == "bar" + + @pytest.mark.respx(base_url=base_url) + async def test_union_response_different_types(self, respx_mock: MockRouter, async_client: AsyncRunloop) -> None: + """Union of objects with the same field name using a different type""" + + class Model1(BaseModel): + foo: int + + class Model2(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model2) + assert response.foo == "bar" + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1})) + + response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model1) + assert response.foo == 1 + + @pytest.mark.respx(base_url=base_url) + async def test_non_application_json_content_type_for_json_data( + self, respx_mock: MockRouter, async_client: AsyncRunloop + ) -> None: + """ + Response that sets Content-Type to something other than application/json but returns json data + """ + + class Model(BaseModel): + foo: int + + respx_mock.get("/foo").mock( + return_value=httpx.Response( + 200, + content=json.dumps({"foo": 2}), + headers={"Content-Type": "application/text"}, + ) + ) + + response = await async_client.get("/foo", cast_to=Model) + assert isinstance(response, Model) + assert response.foo == 2 + + @pytest.mark.respx(base_url=base_url) + async def test_idempotency_header_options(self, respx_mock: MockRouter, async_client: AsyncRunloop) -> None: + respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={})) + + response = await async_client.post("/foo", cast_to=httpx.Response) + + header = response.request.headers.get("x-request-id") + assert header is not None + assert header.startswith("stainless-python-retry") + + # explicit header + response = await async_client.post( + "/foo", cast_to=httpx.Response, options=make_request_options(extra_headers={"x-request-id": "custom-key"}) + ) + assert response.request.headers.get("x-request-id") == "custom-key" + + response = await async_client.post( + "/foo", cast_to=httpx.Response, options=make_request_options(extra_headers={"x-request-id": "custom-key"}) + ) + assert response.request.headers.get("x-request-id") == "custom-key" + + # custom argument + response = await async_client.post( + "/foo", cast_to=httpx.Response, options=make_request_options(idempotency_key="custom-key") + ) + assert response.request.headers.get("x-request-id") == "custom-key" + + async def test_base_url_setter(self) -> None: + client = AsyncRunloop( + base_url="https://example.com/from_init", bearer_token=bearer_token, _strict_response_validation=True + ) + assert client.base_url == "https://example.com/from_init/" + + client.base_url = "https://example.com/from_setter" # type: ignore[assignment] + + assert client.base_url == "https://example.com/from_setter/" + + await client.close() + + async def test_base_url_env(self) -> None: + with update_env(RUNLOOP_BASE_URL="http://localhost:5000/from/env"): + client = AsyncRunloop(bearer_token=bearer_token, _strict_response_validation=True) + assert client.base_url == "http://localhost:5000/from/env/" + + @pytest.mark.parametrize( + "client", + [ + AsyncRunloop( + base_url="http://localhost:5000/custom/path/", + bearer_token=bearer_token, + _strict_response_validation=True, + ), + AsyncRunloop( + base_url="http://localhost:5000/custom/path/", + bearer_token=bearer_token, + _strict_response_validation=True, + http_client=httpx.AsyncClient(), + ), + ], + ids=["standard", "custom http client"], + ) + async def test_base_url_trailing_slash(self, client: AsyncRunloop) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "http://localhost:5000/custom/path/foo" + await client.close() + + @pytest.mark.parametrize( + "client", + [ + AsyncRunloop( + base_url="http://localhost:5000/custom/path/", + bearer_token=bearer_token, + _strict_response_validation=True, + ), + AsyncRunloop( + base_url="http://localhost:5000/custom/path/", + bearer_token=bearer_token, + _strict_response_validation=True, + http_client=httpx.AsyncClient(), + ), + ], + ids=["standard", "custom http client"], + ) + async def test_base_url_no_trailing_slash(self, client: AsyncRunloop) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "http://localhost:5000/custom/path/foo" + await client.close() + + @pytest.mark.parametrize( + "client", + [ + AsyncRunloop( + base_url="http://localhost:5000/custom/path/", + bearer_token=bearer_token, + _strict_response_validation=True, + ), + AsyncRunloop( + base_url="http://localhost:5000/custom/path/", + bearer_token=bearer_token, + _strict_response_validation=True, + http_client=httpx.AsyncClient(), + ), + ], + ids=["standard", "custom http client"], + ) + async def test_absolute_request_url(self, client: AsyncRunloop) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="https://myapi.com/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "https://myapi.com/foo" + await client.close() + + async def test_copied_client_does_not_close_http(self) -> None: + test_client = AsyncRunloop(base_url=base_url, bearer_token=bearer_token, _strict_response_validation=True) + assert not test_client.is_closed() + + copied = test_client.copy() + assert copied is not test_client + + del copied + + await asyncio.sleep(0.2) + assert not test_client.is_closed() + + async def test_client_context_manager(self) -> None: + test_client = AsyncRunloop(base_url=base_url, bearer_token=bearer_token, _strict_response_validation=True) + async with test_client as c2: + assert c2 is test_client + assert not c2.is_closed() + assert not test_client.is_closed() + assert test_client.is_closed() + + @pytest.mark.respx(base_url=base_url) + async def test_client_response_validation_error(self, respx_mock: MockRouter, async_client: AsyncRunloop) -> None: + class Model(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}})) + + with pytest.raises(APIResponseValidationError) as exc: + await async_client.get("/foo", cast_to=Model) + + assert isinstance(exc.value.__cause__, ValidationError) + + async def test_client_max_retries_validation(self) -> None: + with pytest.raises(TypeError, match=r"max_retries cannot be None"): + AsyncRunloop( + base_url=base_url, + bearer_token=bearer_token, + _strict_response_validation=True, + max_retries=cast(Any, None), + ) + + @pytest.mark.respx(base_url=base_url) + async def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: + class Model(BaseModel): + name: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) + + strict_client = AsyncRunloop(base_url=base_url, bearer_token=bearer_token, _strict_response_validation=True) + + with pytest.raises(APIResponseValidationError): + await strict_client.get("/foo", cast_to=Model) + + non_strict_client = AsyncRunloop( + base_url=base_url, bearer_token=bearer_token, _strict_response_validation=False + ) + + response = await non_strict_client.get("/foo", cast_to=Model) + assert isinstance(response, str) # type: ignore[unreachable] + + await strict_client.close() + await non_strict_client.close() + + @pytest.mark.parametrize( + "remaining_retries,retry_after,timeout", + [ + [3, "20", 20], + [3, "0", 1], + [3, "-10", 1], + [3, "60", 60], + [3, "61", 1], + [3, "Fri, 29 Sep 2023 16:26:57 GMT", 20], + [3, "Fri, 29 Sep 2023 16:26:37 GMT", 1], + [3, "Fri, 29 Sep 2023 16:26:27 GMT", 1], + [3, "Fri, 29 Sep 2023 16:27:37 GMT", 60], + [3, "Fri, 29 Sep 2023 16:27:38 GMT", 1], + [3, "99999999999999999999999999999999999", 1], + [3, "Zun, 29 Sep 2023 16:26:27 GMT", 1], + [3, "", 1], + [2, "", 1 * 2.0], + [1, "", 1 * 4.0], + [-1100, "", 60], # test large number potentially overflowing + ], + ) + @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) + async def test_parse_retry_after_header( + self, remaining_retries: int, retry_after: str, timeout: float, async_client: AsyncRunloop + ) -> None: + headers = httpx.Headers({"retry-after": retry_after}) + options = FinalRequestOptions(method="get", url="/foo", max_retries=3) + calculated = async_client._calculate_retry_timeout(remaining_retries, options, headers) + assert calculated == pytest.approx(timeout, 1 * 0.875) # pyright: ignore[reportUnknownMemberType] + + @mock.patch("runloop_api_client._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + async def test_retrying_timeout_errors_doesnt_leak( + self, respx_mock: MockRouter, async_client: AsyncRunloop + ) -> None: + respx_mock.post("/v1/devboxes").mock(side_effect=httpx.TimeoutException("Test timeout error")) + + with pytest.raises(APITimeoutError): + await async_client.devboxes.with_streaming_response.create().__aenter__() + + assert _get_open_connections(async_client) == 0 + + @mock.patch("runloop_api_client._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, async_client: AsyncRunloop) -> None: + respx_mock.post("/v1/devboxes").mock(return_value=httpx.Response(500)) + + with pytest.raises(APIStatusError): + await async_client.devboxes.with_streaming_response.create().__aenter__() + assert _get_open_connections(async_client) == 0 + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("runloop_api_client._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + @pytest.mark.parametrize("failure_mode", ["status", "exception"]) + async def test_retries_taken( + self, + async_client: AsyncRunloop, + failures_before_success: int, + failure_mode: Literal["status", "exception"], + respx_mock: MockRouter, + ) -> None: + client = async_client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + if failure_mode == "exception": + raise RuntimeError("oops") + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.post("/v1/devboxes").mock(side_effect=retry_handler) + + response = await client.devboxes.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 + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("runloop_api_client._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + async def test_omit_retry_count_header( + self, async_client: AsyncRunloop, failures_before_success: int, respx_mock: MockRouter + ) -> None: + client = async_client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.post("/v1/devboxes").mock(side_effect=retry_handler) + + response = await client.devboxes.with_raw_response.create(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("runloop_api_client._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + async def test_overwrite_retry_count_header( + self, async_client: AsyncRunloop, failures_before_success: int, respx_mock: MockRouter + ) -> None: + client = async_client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.post("/v1/devboxes").mock(side_effect=retry_handler) + + response = await client.devboxes.with_raw_response.create(extra_headers={"x-stainless-retry-count": "42"}) + + assert response.http_request.headers.get("x-stainless-retry-count") == "42" + + async def test_get_platform(self) -> None: + platform = await asyncify(get_platform)() + assert isinstance(platform, (str, OtherPlatform)) + + async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: + # Test that the proxy environment variables are set correctly + monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + # Delete in case our environment has any proxy env vars set + monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.delenv("NO_PROXY", raising=False) + monkeypatch.delenv("http_proxy", raising=False) + monkeypatch.delenv("https_proxy", raising=False) + monkeypatch.delenv("all_proxy", raising=False) + monkeypatch.delenv("no_proxy", raising=False) + + client = DefaultAsyncHttpxClient() + + mounts = tuple(client._mounts.items()) + assert len(mounts) == 1 + assert mounts[0][0].pattern == "https://" + + @pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning") + async def test_default_client_creation(self) -> None: + # Ensure that the client can be initialized without any exceptions + DefaultAsyncHttpxClient( + verify=True, + cert=None, + trust_env=True, + http1=True, + http2=False, + limits=httpx.Limits(max_connections=100, max_keepalive_connections=20), + ) + + @pytest.mark.respx(base_url=base_url) + async def test_follow_redirects(self, respx_mock: MockRouter, async_client: AsyncRunloop) -> None: + # Test that the default follow_redirects=True allows following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"})) + + response = await async_client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) + assert response.status_code == 200 + assert response.json() == {"status": "ok"} + + @pytest.mark.respx(base_url=base_url) + async def test_follow_redirects_disabled(self, respx_mock: MockRouter, async_client: AsyncRunloop) -> None: + # Test that follow_redirects=False prevents following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + + with pytest.raises(APIStatusError) as exc_info: + await async_client.post( + "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response + ) + + assert exc_info.value.response.status_code == 302 + assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected" diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py new file mode 100644 index 000000000..89eb51864 --- /dev/null +++ b/tests/test_deepcopy.py @@ -0,0 +1,58 @@ +from runloop_api_client._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 000000000..41498b836 --- /dev/null +++ b/tests/test_extract_files.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +from typing import Sequence + +import pytest + +from runloop_api_client._types import FileTypes +from runloop_api_client._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 000000000..6b19b3721 --- /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 runloop_api_client._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 000000000..aac33d2e4 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,963 @@ +import json +from typing import TYPE_CHECKING, Any, Dict, List, Union, Optional, cast +from datetime import datetime, timezone +from typing_extensions import Literal, Annotated, TypeAliasType + +import pytest +import pydantic +from pydantic import Field + +from runloop_api_client._utils import PropertyInfo +from runloop_api_client._compat import PYDANTIC_V1, parse_obj, model_dump, model_json +from runloop_api_client._models import DISCRIMINATOR_CACHE, BaseModel, construct_type + + +class BasicModel(BaseModel): + foo: str + + +@pytest.mark.parametrize("value", ["hello", 1], ids=["correct type", "mismatched"]) +def test_basic(value: object) -> None: + m = BasicModel.construct(foo=value) + assert m.foo == value + + +def test_directly_nested_model() -> None: + class NestedModel(BaseModel): + nested: BasicModel + + m = NestedModel.construct(nested={"foo": "Foo!"}) + assert m.nested.foo == "Foo!" + + # mismatched types + m = NestedModel.construct(nested="hello!") + assert cast(Any, m.nested) == "hello!" + + +def test_optional_nested_model() -> None: + class NestedModel(BaseModel): + nested: Optional[BasicModel] + + m1 = NestedModel.construct(nested=None) + assert m1.nested is None + + m2 = NestedModel.construct(nested={"foo": "bar"}) + assert m2.nested is not None + assert m2.nested.foo == "bar" + + # mismatched types + m3 = NestedModel.construct(nested={"foo"}) + assert isinstance(cast(Any, m3.nested), set) + assert cast(Any, m3.nested) == {"foo"} + + +def test_list_nested_model() -> None: + class NestedModel(BaseModel): + nested: List[BasicModel] + + m = NestedModel.construct(nested=[{"foo": "bar"}, {"foo": "2"}]) + assert m.nested is not None + assert isinstance(m.nested, list) + assert len(m.nested) == 2 + assert m.nested[0].foo == "bar" + assert m.nested[1].foo == "2" + + # mismatched types + m = NestedModel.construct(nested=True) + assert cast(Any, m.nested) is True + + m = NestedModel.construct(nested=[False]) + assert cast(Any, m.nested) == [False] + + +def test_optional_list_nested_model() -> None: + class NestedModel(BaseModel): + nested: Optional[List[BasicModel]] + + m1 = NestedModel.construct(nested=[{"foo": "bar"}, {"foo": "2"}]) + assert m1.nested is not None + assert isinstance(m1.nested, list) + assert len(m1.nested) == 2 + assert m1.nested[0].foo == "bar" + assert m1.nested[1].foo == "2" + + m2 = NestedModel.construct(nested=None) + assert m2.nested is None + + # mismatched types + m3 = NestedModel.construct(nested={1}) + assert cast(Any, m3.nested) == {1} + + m4 = NestedModel.construct(nested=[False]) + assert cast(Any, m4.nested) == [False] + + +def test_list_optional_items_nested_model() -> None: + class NestedModel(BaseModel): + nested: List[Optional[BasicModel]] + + m = NestedModel.construct(nested=[None, {"foo": "bar"}]) + assert m.nested is not None + assert isinstance(m.nested, list) + assert len(m.nested) == 2 + assert m.nested[0] is None + assert m.nested[1] is not None + assert m.nested[1].foo == "bar" + + # mismatched types + m3 = NestedModel.construct(nested="foo") + assert cast(Any, m3.nested) == "foo" + + m4 = NestedModel.construct(nested=[False]) + assert cast(Any, m4.nested) == [False] + + +def test_list_mismatched_type() -> None: + class NestedModel(BaseModel): + nested: List[str] + + m = NestedModel.construct(nested=False) + assert cast(Any, m.nested) is False + + +def test_raw_dictionary() -> None: + class NestedModel(BaseModel): + nested: Dict[str, str] + + m = NestedModel.construct(nested={"hello": "world"}) + assert m.nested == {"hello": "world"} + + # mismatched types + m = NestedModel.construct(nested=False) + assert cast(Any, m.nested) is False + + +def test_nested_dictionary_model() -> None: + class NestedModel(BaseModel): + nested: Dict[str, BasicModel] + + m = NestedModel.construct(nested={"hello": {"foo": "bar"}}) + assert isinstance(m.nested, dict) + assert m.nested["hello"].foo == "bar" + + # mismatched types + m = NestedModel.construct(nested={"hello": False}) + assert cast(Any, m.nested["hello"]) is False + + +def test_unknown_fields() -> None: + m1 = BasicModel.construct(foo="foo", unknown=1) + assert m1.foo == "foo" + assert cast(Any, m1).unknown == 1 + + m2 = BasicModel.construct(foo="foo", unknown={"foo_bar": True}) + assert m2.foo == "foo" + assert cast(Any, m2).unknown == {"foo_bar": True} + + assert model_dump(m2) == {"foo": "foo", "unknown": {"foo_bar": True}} + + +def test_strict_validation_unknown_fields() -> None: + class Model(BaseModel): + foo: str + + model = parse_obj(Model, dict(foo="hello!", user="Robert")) + assert model.foo == "hello!" + assert cast(Any, model).user == "Robert" + + assert model_dump(model) == {"foo": "hello!", "user": "Robert"} + + +def test_aliases() -> None: + class Model(BaseModel): + my_field: int = Field(alias="myField") + + m = Model.construct(myField=1) + assert m.my_field == 1 + + # mismatched types + m = Model.construct(myField={"hello": False}) + assert cast(Any, m.my_field) == {"hello": False} + + +def test_repr() -> None: + model = BasicModel(foo="bar") + assert str(model) == "BasicModel(foo='bar')" + assert repr(model) == "BasicModel(foo='bar')" + + +def test_repr_nested_model() -> None: + class Child(BaseModel): + name: str + age: int + + class Parent(BaseModel): + name: str + child: Child + + model = Parent(name="Robert", child=Child(name="Foo", age=5)) + assert str(model) == "Parent(name='Robert', child=Child(name='Foo', age=5))" + assert repr(model) == "Parent(name='Robert', child=Child(name='Foo', age=5))" + + +def test_optional_list() -> None: + class Submodel(BaseModel): + name: str + + class Model(BaseModel): + items: Optional[List[Submodel]] + + m = Model.construct(items=None) + assert m.items is None + + m = Model.construct(items=[]) + assert m.items == [] + + m = Model.construct(items=[{"name": "Robert"}]) + assert m.items is not None + assert len(m.items) == 1 + assert m.items[0].name == "Robert" + + +def test_nested_union_of_models() -> None: + class Submodel1(BaseModel): + bar: bool + + class Submodel2(BaseModel): + thing: str + + class Model(BaseModel): + foo: Union[Submodel1, Submodel2] + + m = Model.construct(foo={"thing": "hello"}) + assert isinstance(m.foo, Submodel2) + assert m.foo.thing == "hello" + + +def test_nested_union_of_mixed_types() -> None: + class Submodel1(BaseModel): + bar: bool + + class Model(BaseModel): + foo: Union[Submodel1, Literal[True], Literal["CARD_HOLDER"]] + + m = Model.construct(foo=True) + assert m.foo is True + + m = Model.construct(foo="CARD_HOLDER") + assert m.foo == "CARD_HOLDER" + + m = Model.construct(foo={"bar": False}) + assert isinstance(m.foo, Submodel1) + assert m.foo.bar is False + + +def test_nested_union_multiple_variants() -> None: + class Submodel1(BaseModel): + bar: bool + + class Submodel2(BaseModel): + thing: str + + class Submodel3(BaseModel): + foo: int + + class Model(BaseModel): + foo: Union[Submodel1, Submodel2, None, Submodel3] + + m = Model.construct(foo={"thing": "hello"}) + assert isinstance(m.foo, Submodel2) + assert m.foo.thing == "hello" + + m = Model.construct(foo=None) + assert m.foo is None + + m = Model.construct() + assert m.foo is None + + m = Model.construct(foo={"foo": "1"}) + assert isinstance(m.foo, Submodel3) + assert m.foo.foo == 1 + + +def test_nested_union_invalid_data() -> None: + class Submodel1(BaseModel): + level: int + + class Submodel2(BaseModel): + name: str + + class Model(BaseModel): + foo: Union[Submodel1, Submodel2] + + m = Model.construct(foo=True) + assert cast(bool, m.foo) is True + + m = Model.construct(foo={"name": 3}) + if PYDANTIC_V1: + assert isinstance(m.foo, Submodel2) + assert m.foo.name == "3" + else: + assert isinstance(m.foo, Submodel1) + assert m.foo.name == 3 # type: ignore + + +def test_list_of_unions() -> None: + class Submodel1(BaseModel): + level: int + + class Submodel2(BaseModel): + name: str + + class Model(BaseModel): + items: List[Union[Submodel1, Submodel2]] + + m = Model.construct(items=[{"level": 1}, {"name": "Robert"}]) + assert len(m.items) == 2 + assert isinstance(m.items[0], Submodel1) + assert m.items[0].level == 1 + assert isinstance(m.items[1], Submodel2) + assert m.items[1].name == "Robert" + + m = Model.construct(items=[{"level": -1}, 156]) + assert len(m.items) == 2 + assert isinstance(m.items[0], Submodel1) + assert m.items[0].level == -1 + assert cast(Any, m.items[1]) == 156 + + +def test_union_of_lists() -> None: + class SubModel1(BaseModel): + level: int + + class SubModel2(BaseModel): + name: str + + class Model(BaseModel): + items: Union[List[SubModel1], List[SubModel2]] + + # with one valid entry + m = Model.construct(items=[{"name": "Robert"}]) + assert len(m.items) == 1 + assert isinstance(m.items[0], SubModel2) + assert m.items[0].name == "Robert" + + # with two entries pointing to different types + m = Model.construct(items=[{"level": 1}, {"name": "Robert"}]) + assert len(m.items) == 2 + assert isinstance(m.items[0], SubModel1) + assert m.items[0].level == 1 + assert isinstance(m.items[1], SubModel1) + assert cast(Any, m.items[1]).name == "Robert" + + # with two entries pointing to *completely* different types + m = Model.construct(items=[{"level": -1}, 156]) + assert len(m.items) == 2 + assert isinstance(m.items[0], SubModel1) + assert m.items[0].level == -1 + assert cast(Any, m.items[1]) == 156 + + +def test_dict_of_union() -> None: + class SubModel1(BaseModel): + name: str + + class SubModel2(BaseModel): + foo: str + + class Model(BaseModel): + data: Dict[str, Union[SubModel1, SubModel2]] + + m = Model.construct(data={"hello": {"name": "there"}, "foo": {"foo": "bar"}}) + assert len(list(m.data.keys())) == 2 + assert isinstance(m.data["hello"], SubModel1) + assert m.data["hello"].name == "there" + assert isinstance(m.data["foo"], SubModel2) + assert m.data["foo"].foo == "bar" + + # TODO: test mismatched type + + +def test_double_nested_union() -> None: + class SubModel1(BaseModel): + name: str + + class SubModel2(BaseModel): + bar: str + + class Model(BaseModel): + data: Dict[str, List[Union[SubModel1, SubModel2]]] + + m = Model.construct(data={"foo": [{"bar": "baz"}, {"name": "Robert"}]}) + assert len(m.data["foo"]) == 2 + + entry1 = m.data["foo"][0] + assert isinstance(entry1, SubModel2) + assert entry1.bar == "baz" + + entry2 = m.data["foo"][1] + assert isinstance(entry2, SubModel1) + assert entry2.name == "Robert" + + # TODO: test mismatched type + + +def test_union_of_dict() -> None: + class SubModel1(BaseModel): + name: str + + class SubModel2(BaseModel): + foo: str + + class Model(BaseModel): + data: Union[Dict[str, SubModel1], Dict[str, SubModel2]] + + m = Model.construct(data={"hello": {"name": "there"}, "foo": {"foo": "bar"}}) + assert len(list(m.data.keys())) == 2 + assert isinstance(m.data["hello"], SubModel1) + assert m.data["hello"].name == "there" + assert isinstance(m.data["foo"], SubModel1) + assert cast(Any, m.data["foo"]).foo == "bar" + + +def test_iso8601_datetime() -> None: + class Model(BaseModel): + created_at: datetime + + expected = datetime(2019, 12, 27, 18, 11, 19, 117000, tzinfo=timezone.utc) + + if PYDANTIC_V1: + expected_json = '{"created_at": "2019-12-27T18:11:19.117000+00:00"}' + else: + expected_json = '{"created_at":"2019-12-27T18:11:19.117000Z"}' + + model = Model.construct(created_at="2019-12-27T18:11:19.117Z") + assert model.created_at == expected + assert model_json(model) == expected_json + + model = parse_obj(Model, dict(created_at="2019-12-27T18:11:19.117Z")) + assert model.created_at == expected + assert model_json(model) == expected_json + + +def test_does_not_coerce_int() -> None: + class Model(BaseModel): + bar: int + + assert Model.construct(bar=1).bar == 1 + assert Model.construct(bar=10.9).bar == 10.9 + assert Model.construct(bar="19").bar == "19" # type: ignore[comparison-overlap] + assert Model.construct(bar=False).bar is False + + +def test_int_to_float_safe_conversion() -> None: + class Model(BaseModel): + float_field: float + + m = Model.construct(float_field=10) + assert m.float_field == 10.0 + assert isinstance(m.float_field, float) + + m = Model.construct(float_field=10.12) + assert m.float_field == 10.12 + assert isinstance(m.float_field, float) + + # number too big + m = Model.construct(float_field=2**53 + 1) + assert m.float_field == 2**53 + 1 + assert isinstance(m.float_field, int) + + +def test_deprecated_alias() -> None: + class Model(BaseModel): + resource_id: str = Field(alias="model_id") + + @property + def model_id(self) -> str: + return self.resource_id + + m = Model.construct(model_id="id") + assert m.model_id == "id" + assert m.resource_id == "id" + assert m.resource_id is m.model_id + + m = parse_obj(Model, {"model_id": "id"}) + assert m.model_id == "id" + assert m.resource_id == "id" + assert m.resource_id is m.model_id + + +def test_omitted_fields() -> None: + class Model(BaseModel): + resource_id: Optional[str] = None + + m = Model.construct() + assert m.resource_id is None + assert "resource_id" not in m.model_fields_set + + m = Model.construct(resource_id=None) + assert m.resource_id is None + assert "resource_id" in m.model_fields_set + + m = Model.construct(resource_id="foo") + assert m.resource_id == "foo" + assert "resource_id" in m.model_fields_set + + +def test_to_dict() -> None: + class Model(BaseModel): + foo: Optional[str] = Field(alias="FOO", default=None) + + m = Model(FOO="hello") + assert m.to_dict() == {"FOO": "hello"} + assert m.to_dict(use_api_names=False) == {"foo": "hello"} + + m2 = Model() + assert m2.to_dict() == {} + assert m2.to_dict(exclude_unset=False) == {"FOO": None} + assert m2.to_dict(exclude_unset=False, exclude_none=True) == {} + assert m2.to_dict(exclude_unset=False, exclude_defaults=True) == {} + + m3 = Model(FOO=None) + assert m3.to_dict() == {"FOO": None} + assert m3.to_dict(exclude_none=True) == {} + assert m3.to_dict(exclude_defaults=True) == {} + + class Model2(BaseModel): + created_at: datetime + + time_str = "2024-03-21T11:39:01.275859" + m4 = Model2.construct(created_at=time_str) + assert m4.to_dict(mode="python") == {"created_at": datetime.fromisoformat(time_str)} + assert m4.to_dict(mode="json") == {"created_at": time_str} + + if PYDANTIC_V1: + with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): + m.to_dict(warnings=False) + + +def test_forwards_compat_model_dump_method() -> None: + class Model(BaseModel): + foo: Optional[str] = Field(alias="FOO", default=None) + + m = Model(FOO="hello") + assert m.model_dump() == {"foo": "hello"} + assert m.model_dump(include={"bar"}) == {} + assert m.model_dump(exclude={"foo"}) == {} + assert m.model_dump(by_alias=True) == {"FOO": "hello"} + + m2 = Model() + assert m2.model_dump() == {"foo": None} + assert m2.model_dump(exclude_unset=True) == {} + assert m2.model_dump(exclude_none=True) == {} + assert m2.model_dump(exclude_defaults=True) == {} + + m3 = Model(FOO=None) + assert m3.model_dump() == {"foo": None} + assert m3.model_dump(exclude_none=True) == {} + + if PYDANTIC_V1: + with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"): + m.model_dump(round_trip=True) + + with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): + m.model_dump(warnings=False) + + +def test_compat_method_no_error_for_warnings() -> None: + class Model(BaseModel): + foo: Optional[str] + + m = Model(foo="hello") + assert isinstance(model_dump(m, warnings=False), dict) + + +def test_to_json() -> None: + class Model(BaseModel): + foo: Optional[str] = Field(alias="FOO", default=None) + + m = Model(FOO="hello") + assert json.loads(m.to_json()) == {"FOO": "hello"} + assert json.loads(m.to_json(use_api_names=False)) == {"foo": "hello"} + + if PYDANTIC_V1: + assert m.to_json(indent=None) == '{"FOO": "hello"}' + else: + assert m.to_json(indent=None) == '{"FOO":"hello"}' + + m2 = Model() + assert json.loads(m2.to_json()) == {} + assert json.loads(m2.to_json(exclude_unset=False)) == {"FOO": None} + assert json.loads(m2.to_json(exclude_unset=False, exclude_none=True)) == {} + assert json.loads(m2.to_json(exclude_unset=False, exclude_defaults=True)) == {} + + m3 = Model(FOO=None) + assert json.loads(m3.to_json()) == {"FOO": None} + assert json.loads(m3.to_json(exclude_none=True)) == {} + + if PYDANTIC_V1: + with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): + m.to_json(warnings=False) + + +def test_forwards_compat_model_dump_json_method() -> None: + class Model(BaseModel): + foo: Optional[str] = Field(alias="FOO", default=None) + + m = Model(FOO="hello") + assert json.loads(m.model_dump_json()) == {"foo": "hello"} + assert json.loads(m.model_dump_json(include={"bar"})) == {} + assert json.loads(m.model_dump_json(include={"foo"})) == {"foo": "hello"} + assert json.loads(m.model_dump_json(by_alias=True)) == {"FOO": "hello"} + + assert m.model_dump_json(indent=2) == '{\n "foo": "hello"\n}' + + m2 = Model() + assert json.loads(m2.model_dump_json()) == {"foo": None} + assert json.loads(m2.model_dump_json(exclude_unset=True)) == {} + assert json.loads(m2.model_dump_json(exclude_none=True)) == {} + assert json.loads(m2.model_dump_json(exclude_defaults=True)) == {} + + m3 = Model(FOO=None) + assert json.loads(m3.model_dump_json()) == {"foo": None} + assert json.loads(m3.model_dump_json(exclude_none=True)) == {} + + if PYDANTIC_V1: + with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"): + m.model_dump_json(round_trip=True) + + with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): + m.model_dump_json(warnings=False) + + +def test_type_compat() -> None: + # our model type can be assigned to Pydantic's model type + + def takes_pydantic(model: pydantic.BaseModel) -> None: # noqa: ARG001 + ... + + class OurModel(BaseModel): + foo: Optional[str] = None + + takes_pydantic(OurModel()) + + +def test_annotated_types() -> None: + class Model(BaseModel): + value: str + + m = construct_type( + value={"value": "foo"}, + type_=cast(Any, Annotated[Model, "random metadata"]), + ) + assert isinstance(m, Model) + assert m.value == "foo" + + +def test_discriminated_unions_invalid_data() -> None: + class A(BaseModel): + type: Literal["a"] + + data: str + + class B(BaseModel): + type: Literal["b"] + + data: int + + m = construct_type( + value={"type": "b", "data": "foo"}, + type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="type")]), + ) + assert isinstance(m, B) + assert m.type == "b" + assert m.data == "foo" # type: ignore[comparison-overlap] + + m = construct_type( + value={"type": "a", "data": 100}, + type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="type")]), + ) + assert isinstance(m, A) + assert m.type == "a" + if PYDANTIC_V1: + # pydantic v1 automatically converts inputs to strings + # if the expected type is a str + assert m.data == "100" + else: + assert m.data == 100 # type: ignore[comparison-overlap] + + +def test_discriminated_unions_unknown_variant() -> None: + class A(BaseModel): + type: Literal["a"] + + data: str + + class B(BaseModel): + type: Literal["b"] + + data: int + + m = construct_type( + value={"type": "c", "data": None, "new_thing": "bar"}, + type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="type")]), + ) + + # just chooses the first variant + assert isinstance(m, A) + assert m.type == "c" # type: ignore[comparison-overlap] + assert m.data == None # type: ignore[unreachable] + assert m.new_thing == "bar" + + +def test_discriminated_unions_invalid_data_nested_unions() -> None: + class A(BaseModel): + type: Literal["a"] + + data: str + + class B(BaseModel): + type: Literal["b"] + + data: int + + class C(BaseModel): + type: Literal["c"] + + data: bool + + m = construct_type( + value={"type": "b", "data": "foo"}, + type_=cast(Any, Annotated[Union[Union[A, B], C], PropertyInfo(discriminator="type")]), + ) + assert isinstance(m, B) + assert m.type == "b" + assert m.data == "foo" # type: ignore[comparison-overlap] + + m = construct_type( + value={"type": "c", "data": "foo"}, + type_=cast(Any, Annotated[Union[Union[A, B], C], PropertyInfo(discriminator="type")]), + ) + assert isinstance(m, C) + assert m.type == "c" + assert m.data == "foo" # type: ignore[comparison-overlap] + + +def test_discriminated_unions_with_aliases_invalid_data() -> None: + class A(BaseModel): + foo_type: Literal["a"] = Field(alias="type") + + data: str + + class B(BaseModel): + foo_type: Literal["b"] = Field(alias="type") + + data: int + + m = construct_type( + value={"type": "b", "data": "foo"}, + type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="foo_type")]), + ) + assert isinstance(m, B) + assert m.foo_type == "b" + assert m.data == "foo" # type: ignore[comparison-overlap] + + m = construct_type( + value={"type": "a", "data": 100}, + type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="foo_type")]), + ) + assert isinstance(m, A) + assert m.foo_type == "a" + if PYDANTIC_V1: + # pydantic v1 automatically converts inputs to strings + # if the expected type is a str + assert m.data == "100" + else: + assert m.data == 100 # type: ignore[comparison-overlap] + + +def test_discriminated_unions_overlapping_discriminators_invalid_data() -> None: + class A(BaseModel): + type: Literal["a"] + + data: bool + + class B(BaseModel): + type: Literal["a"] + + data: int + + m = construct_type( + value={"type": "a", "data": "foo"}, + type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="type")]), + ) + assert isinstance(m, B) + assert m.type == "a" + assert m.data == "foo" # type: ignore[comparison-overlap] + + +def test_discriminated_unions_invalid_data_uses_cache() -> None: + class A(BaseModel): + type: Literal["a"] + + data: str + + class B(BaseModel): + type: Literal["b"] + + data: int + + UnionType = cast(Any, Union[A, B]) + + assert not DISCRIMINATOR_CACHE.get(UnionType) + + m = construct_type( + value={"type": "b", "data": "foo"}, type_=cast(Any, Annotated[UnionType, PropertyInfo(discriminator="type")]) + ) + assert isinstance(m, B) + assert m.type == "b" + assert m.data == "foo" # type: ignore[comparison-overlap] + + discriminator = DISCRIMINATOR_CACHE.get(UnionType) + assert discriminator is not None + + m = construct_type( + value={"type": "b", "data": "foo"}, type_=cast(Any, Annotated[UnionType, PropertyInfo(discriminator="type")]) + ) + assert isinstance(m, B) + assert m.type == "b" + assert m.data == "foo" # type: ignore[comparison-overlap] + + # if the discriminator details object stays the same between invocations then + # we hit the cache + assert DISCRIMINATOR_CACHE.get(UnionType) is discriminator + + +@pytest.mark.skipif(PYDANTIC_V1, reason="TypeAliasType is not supported in Pydantic v1") +def test_type_alias_type() -> None: + Alias = TypeAliasType("Alias", str) # pyright: ignore + + class Model(BaseModel): + alias: Alias + union: Union[int, Alias] + + m = construct_type(value={"alias": "foo", "union": "bar"}, type_=Model) + assert isinstance(m, Model) + assert isinstance(m.alias, str) + assert m.alias == "foo" + assert isinstance(m.union, str) + assert m.union == "bar" + + +@pytest.mark.skipif(PYDANTIC_V1, reason="TypeAliasType is not supported in Pydantic v1") +def test_field_named_cls() -> None: + class Model(BaseModel): + cls: str + + m = construct_type(value={"cls": "foo"}, type_=Model) + assert isinstance(m, Model) + assert isinstance(m.cls, str) + + +def test_discriminated_union_case() -> None: + class A(BaseModel): + type: Literal["a"] + + data: bool + + class B(BaseModel): + type: Literal["b"] + + data: List[Union[A, object]] + + class ModelA(BaseModel): + type: Literal["modelA"] + + data: int + + class ModelB(BaseModel): + type: Literal["modelB"] + + required: str + + data: Union[A, B] + + # when constructing ModelA | ModelB, value data doesn't match ModelB exactly - missing `required` + m = construct_type( + value={"type": "modelB", "data": {"type": "a", "data": True}}, + type_=cast(Any, Annotated[Union[ModelA, ModelB], PropertyInfo(discriminator="type")]), + ) + + assert isinstance(m, ModelB) + + +def test_nested_discriminated_union() -> None: + class InnerType1(BaseModel): + type: Literal["type_1"] + + class InnerModel(BaseModel): + inner_value: str + + class InnerType2(BaseModel): + type: Literal["type_2"] + some_inner_model: InnerModel + + class Type1(BaseModel): + base_type: Literal["base_type_1"] + value: Annotated[ + Union[ + InnerType1, + InnerType2, + ], + PropertyInfo(discriminator="type"), + ] + + class Type2(BaseModel): + base_type: Literal["base_type_2"] + + T = Annotated[ + Union[ + Type1, + Type2, + ], + PropertyInfo(discriminator="base_type"), + ] + + model = construct_type( + type_=T, + value={ + "base_type": "base_type_1", + "value": { + "type": "type_2", + }, + }, + ) + assert isinstance(model, Type1) + assert isinstance(model.value, InnerType2) + + +@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2 for now") +def test_extra_properties() -> None: + class Item(BaseModel): + prop: int + + class Model(BaseModel): + __pydantic_extra__: Dict[str, Item] = Field(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + other: str + + if TYPE_CHECKING: + + def __getattr__(self, attr: str) -> Item: ... + + model = construct_type( + type_=Model, + value={ + "a": {"prop": 1}, + "other": "foo", + }, + ) + assert isinstance(model, Model) + assert model.a.prop == 1 + assert isinstance(model.a, Item) + assert model.other == "foo" diff --git a/tests/test_qs.py b/tests/test_qs.py new file mode 100644 index 000000000..34eb9226c --- /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 runloop_api_client._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 000000000..33159a496 --- /dev/null +++ b/tests/test_required_args.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +import pytest + +from runloop_api_client._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 000000000..85e98bae8 --- /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 runloop_api_client import Runloop, BaseModel, AsyncRunloop +from runloop_api_client._response import ( + APIResponse, + BaseAPIResponse, + AsyncAPIResponse, + BinaryAPIResponse, + AsyncBinaryAPIResponse, + extract_response_type, +) +from runloop_api_client._streaming import Stream +from runloop_api_client._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: Runloop) -> 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 runloop_api_client import BaseModel`", + ): + response.parse(to=PydanticModel) + + +@pytest.mark.asyncio +async def test_async_response_parse_mismatched_basemodel(async_client: AsyncRunloop) -> 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 runloop_api_client import BaseModel`", + ): + await response.parse(to=PydanticModel) + + +def test_response_parse_custom_stream(client: Runloop) -> 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: AsyncRunloop) -> 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: Runloop) -> 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: AsyncRunloop) -> 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: Runloop) -> 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: AsyncRunloop) -> 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: Runloop, 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: AsyncRunloop, 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: Runloop) -> 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: AsyncRunloop) -> 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 000000000..e8bf8f093 --- /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 runloop_api_client import Runloop, AsyncRunloop +from runloop_api_client._streaming import Stream, AsyncStream, ServerSentEvent + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_basic(sync: bool, client: Runloop, async_client: AsyncRunloop) -> 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: Runloop, async_client: AsyncRunloop) -> 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: Runloop, async_client: AsyncRunloop) -> 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: Runloop, async_client: AsyncRunloop) -> 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: Runloop, async_client: AsyncRunloop) -> 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: Runloop, async_client: AsyncRunloop) -> 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: Runloop, async_client: AsyncRunloop) -> 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: Runloop, async_client: AsyncRunloop) -> 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: Runloop, + async_client: AsyncRunloop, +) -> 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: Runloop, + async_client: AsyncRunloop, +) -> 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: Runloop, + async_client: AsyncRunloop, +) -> 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 000000000..5256aad7b --- /dev/null +++ b/tests/test_transform.py @@ -0,0 +1,460 @@ +from __future__ import annotations + +import io +import pathlib +from typing import Any, Dict, List, Union, TypeVar, Iterable, Optional, cast +from datetime import date, datetime +from typing_extensions import Required, Annotated, TypedDict + +import pytest + +from runloop_api_client._types import Base64FileInput, omit, not_given +from runloop_api_client._utils import ( + PropertyInfo, + transform as _transform, + parse_datetime, + async_transform as _async_transform, +) +from runloop_api_client._compat import PYDANTIC_V1 +from runloop_api_client._models import BaseModel + +_T = TypeVar("_T") + +SAMPLE_FILE_PATH = pathlib.Path(__file__).parent.joinpath("sample_file.txt") + + +async def transform( + data: _T, + expected_type: object, + use_async: bool, +) -> _T: + if use_async: + return await _async_transform(data, expected_type=expected_type) + + return _transform(data, expected_type=expected_type) + + +parametrize = pytest.mark.parametrize("use_async", [False, True], ids=["sync", "async"]) + + +class Foo1(TypedDict): + foo_bar: Annotated[str, PropertyInfo(alias="fooBar")] + + +@parametrize +@pytest.mark.asyncio +async def test_top_level_alias(use_async: bool) -> None: + assert await transform({"foo_bar": "hello"}, expected_type=Foo1, use_async=use_async) == {"fooBar": "hello"} + + +class Foo2(TypedDict): + bar: Bar2 + + +class Bar2(TypedDict): + this_thing: Annotated[int, PropertyInfo(alias="this__thing")] + baz: Annotated[Baz2, PropertyInfo(alias="Baz")] + + +class Baz2(TypedDict): + my_baz: Annotated[str, PropertyInfo(alias="myBaz")] + + +@parametrize +@pytest.mark.asyncio +async def test_recursive_typeddict(use_async: bool) -> None: + assert await transform({"bar": {"this_thing": 1}}, Foo2, use_async) == {"bar": {"this__thing": 1}} + assert await transform({"bar": {"baz": {"my_baz": "foo"}}}, Foo2, use_async) == {"bar": {"Baz": {"myBaz": "foo"}}} + + +class Foo3(TypedDict): + things: List[Bar3] + + +class Bar3(TypedDict): + my_field: Annotated[str, PropertyInfo(alias="myField")] + + +@parametrize +@pytest.mark.asyncio +async def test_list_of_typeddict(use_async: bool) -> None: + result = await transform({"things": [{"my_field": "foo"}, {"my_field": "foo2"}]}, Foo3, use_async) + assert result == {"things": [{"myField": "foo"}, {"myField": "foo2"}]} + + +class Foo4(TypedDict): + foo: Union[Bar4, Baz4] + + +class Bar4(TypedDict): + foo_bar: Annotated[str, PropertyInfo(alias="fooBar")] + + +class Baz4(TypedDict): + foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] + + +@parametrize +@pytest.mark.asyncio +async def test_union_of_typeddict(use_async: bool) -> None: + assert await transform({"foo": {"foo_bar": "bar"}}, Foo4, use_async) == {"foo": {"fooBar": "bar"}} + assert await transform({"foo": {"foo_baz": "baz"}}, Foo4, use_async) == {"foo": {"fooBaz": "baz"}} + assert await transform({"foo": {"foo_baz": "baz", "foo_bar": "bar"}}, Foo4, use_async) == { + "foo": {"fooBaz": "baz", "fooBar": "bar"} + } + + +class Foo5(TypedDict): + foo: Annotated[Union[Bar4, List[Baz4]], PropertyInfo(alias="FOO")] + + +class Bar5(TypedDict): + foo_bar: Annotated[str, PropertyInfo(alias="fooBar")] + + +class Baz5(TypedDict): + foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] + + +@parametrize +@pytest.mark.asyncio +async def test_union_of_list(use_async: bool) -> None: + assert await transform({"foo": {"foo_bar": "bar"}}, Foo5, use_async) == {"FOO": {"fooBar": "bar"}} + assert await transform( + { + "foo": [ + {"foo_baz": "baz"}, + {"foo_baz": "baz"}, + ] + }, + Foo5, + use_async, + ) == {"FOO": [{"fooBaz": "baz"}, {"fooBaz": "baz"}]} + + +class Foo6(TypedDict): + bar: Annotated[str, PropertyInfo(alias="Bar")] + + +@parametrize +@pytest.mark.asyncio +async def test_includes_unknown_keys(use_async: bool) -> None: + assert await transform({"bar": "bar", "baz_": {"FOO": 1}}, Foo6, use_async) == { + "Bar": "bar", + "baz_": {"FOO": 1}, + } + + +class Foo7(TypedDict): + bar: Annotated[List[Bar7], PropertyInfo(alias="bAr")] + foo: Bar7 + + +class Bar7(TypedDict): + foo: str + + +@parametrize +@pytest.mark.asyncio +async def test_ignores_invalid_input(use_async: bool) -> None: + assert await transform({"bar": ""}, Foo7, use_async) == {"bAr": ""} + assert await transform({"foo": ""}, Foo7, use_async) == {"foo": ""} + + +class DatetimeDict(TypedDict, total=False): + foo: Annotated[datetime, PropertyInfo(format="iso8601")] + + bar: Annotated[Optional[datetime], PropertyInfo(format="iso8601")] + + required: Required[Annotated[Optional[datetime], PropertyInfo(format="iso8601")]] + + list_: Required[Annotated[Optional[List[datetime]], PropertyInfo(format="iso8601")]] + + union: Annotated[Union[int, datetime], PropertyInfo(format="iso8601")] + + +class DateDict(TypedDict, total=False): + foo: Annotated[date, PropertyInfo(format="iso8601")] + + +class DatetimeModel(BaseModel): + foo: datetime + + +class DateModel(BaseModel): + foo: Optional[date] + + +@parametrize +@pytest.mark.asyncio +async def test_iso8601_format(use_async: bool) -> None: + dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") + tz = "+00:00" if PYDANTIC_V1 else "Z" + assert await transform({"foo": dt}, DatetimeDict, use_async) == {"foo": "2023-02-23T14:16:36.337692+00:00"} # type: ignore[comparison-overlap] + assert await transform(DatetimeModel(foo=dt), Any, use_async) == {"foo": "2023-02-23T14:16:36.337692" + tz} # type: ignore[comparison-overlap] + + dt = dt.replace(tzinfo=None) + assert await transform({"foo": dt}, DatetimeDict, use_async) == {"foo": "2023-02-23T14:16:36.337692"} # type: ignore[comparison-overlap] + assert await transform(DatetimeModel(foo=dt), Any, use_async) == {"foo": "2023-02-23T14:16:36.337692"} # type: ignore[comparison-overlap] + + assert await transform({"foo": None}, DateDict, use_async) == {"foo": None} # type: ignore[comparison-overlap] + assert await transform(DateModel(foo=None), Any, use_async) == {"foo": None} # type: ignore + assert await transform({"foo": date.fromisoformat("2023-02-23")}, DateDict, use_async) == {"foo": "2023-02-23"} # type: ignore[comparison-overlap] + assert await transform(DateModel(foo=date.fromisoformat("2023-02-23")), DateDict, use_async) == { + "foo": "2023-02-23" + } # type: ignore[comparison-overlap] + + +@parametrize +@pytest.mark.asyncio +async def test_optional_iso8601_format(use_async: bool) -> None: + dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") + assert await transform({"bar": dt}, DatetimeDict, use_async) == {"bar": "2023-02-23T14:16:36.337692+00:00"} # type: ignore[comparison-overlap] + + assert await transform({"bar": None}, DatetimeDict, use_async) == {"bar": None} + + +@parametrize +@pytest.mark.asyncio +async def test_required_iso8601_format(use_async: bool) -> None: + dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") + assert await transform({"required": dt}, DatetimeDict, use_async) == { + "required": "2023-02-23T14:16:36.337692+00:00" + } # type: ignore[comparison-overlap] + + assert await transform({"required": None}, DatetimeDict, use_async) == {"required": None} + + +@parametrize +@pytest.mark.asyncio +async def test_union_datetime(use_async: bool) -> None: + dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") + assert await transform({"union": dt}, DatetimeDict, use_async) == { # type: ignore[comparison-overlap] + "union": "2023-02-23T14:16:36.337692+00:00" + } + + assert await transform({"union": "foo"}, DatetimeDict, use_async) == {"union": "foo"} + + +@parametrize +@pytest.mark.asyncio +async def test_nested_list_iso6801_format(use_async: bool) -> None: + dt1 = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") + dt2 = parse_datetime("2022-01-15T06:34:23Z") + assert await transform({"list_": [dt1, dt2]}, DatetimeDict, use_async) == { # type: ignore[comparison-overlap] + "list_": ["2023-02-23T14:16:36.337692+00:00", "2022-01-15T06:34:23+00:00"] + } + + +@parametrize +@pytest.mark.asyncio +async def test_datetime_custom_format(use_async: bool) -> None: + dt = parse_datetime("2022-01-15T06:34:23Z") + + result = await transform(dt, Annotated[datetime, PropertyInfo(format="custom", format_template="%H")], use_async) + assert result == "06" # type: ignore[comparison-overlap] + + +class DateDictWithRequiredAlias(TypedDict, total=False): + required_prop: Required[Annotated[date, PropertyInfo(format="iso8601", alias="prop")]] + + +@parametrize +@pytest.mark.asyncio +async def test_datetime_with_alias(use_async: bool) -> None: + assert await transform({"required_prop": None}, DateDictWithRequiredAlias, use_async) == {"prop": None} # type: ignore[comparison-overlap] + assert await transform( + {"required_prop": date.fromisoformat("2023-02-23")}, DateDictWithRequiredAlias, use_async + ) == {"prop": "2023-02-23"} # type: ignore[comparison-overlap] + + +class MyModel(BaseModel): + foo: str + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_model_to_dictionary(use_async: bool) -> None: + assert cast(Any, await transform(MyModel(foo="hi!"), Any, use_async)) == {"foo": "hi!"} + assert cast(Any, await transform(MyModel.construct(foo="hi!"), Any, use_async)) == {"foo": "hi!"} + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_empty_model(use_async: bool) -> None: + assert cast(Any, await transform(MyModel.construct(), Any, use_async)) == {} + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_unknown_field(use_async: bool) -> None: + assert cast(Any, await transform(MyModel.construct(my_untyped_field=True), Any, use_async)) == { + "my_untyped_field": True + } + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_mismatched_types(use_async: bool) -> None: + model = MyModel.construct(foo=True) + if PYDANTIC_V1: + params = await transform(model, Any, use_async) + else: + with pytest.warns(UserWarning): + params = await transform(model, Any, use_async) + assert cast(Any, params) == {"foo": True} + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_mismatched_object_type(use_async: bool) -> None: + model = MyModel.construct(foo=MyModel.construct(hello="world")) + if PYDANTIC_V1: + params = await transform(model, Any, use_async) + else: + with pytest.warns(UserWarning): + params = await transform(model, Any, use_async) + assert cast(Any, params) == {"foo": {"hello": "world"}} + + +class ModelNestedObjects(BaseModel): + nested: MyModel + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_nested_objects(use_async: bool) -> None: + model = ModelNestedObjects.construct(nested={"foo": "stainless"}) + assert isinstance(model.nested, MyModel) + assert cast(Any, await transform(model, Any, use_async)) == {"nested": {"foo": "stainless"}} + + +class ModelWithDefaultField(BaseModel): + foo: str + with_none_default: Union[str, None] = None + with_str_default: str = "foo" + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_default_field(use_async: bool) -> None: + # should be excluded when defaults are used + model = ModelWithDefaultField.construct() + assert model.with_none_default is None + assert model.with_str_default == "foo" + assert cast(Any, await transform(model, Any, use_async)) == {} + + # should be included when the default value is explicitly given + model = ModelWithDefaultField.construct(with_none_default=None, with_str_default="foo") + assert model.with_none_default is None + assert model.with_str_default == "foo" + assert cast(Any, await transform(model, Any, use_async)) == {"with_none_default": None, "with_str_default": "foo"} + + # should be included when a non-default value is explicitly given + model = ModelWithDefaultField.construct(with_none_default="bar", with_str_default="baz") + assert model.with_none_default == "bar" + assert model.with_str_default == "baz" + assert cast(Any, await transform(model, Any, use_async)) == {"with_none_default": "bar", "with_str_default": "baz"} + + +class TypedDictIterableUnion(TypedDict): + foo: Annotated[Union[Bar8, Iterable[Baz8]], PropertyInfo(alias="FOO")] + + +class Bar8(TypedDict): + foo_bar: Annotated[str, PropertyInfo(alias="fooBar")] + + +class Baz8(TypedDict): + foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] + + +@parametrize +@pytest.mark.asyncio +async def test_iterable_of_dictionaries(use_async: bool) -> None: + assert await transform({"foo": [{"foo_baz": "bar"}]}, TypedDictIterableUnion, use_async) == { + "FOO": [{"fooBaz": "bar"}] + } + assert cast(Any, await transform({"foo": ({"foo_baz": "bar"},)}, TypedDictIterableUnion, use_async)) == { + "FOO": [{"fooBaz": "bar"}] + } + + def my_iter() -> Iterable[Baz8]: + yield {"foo_baz": "hello"} + yield {"foo_baz": "world"} + + assert await transform({"foo": my_iter()}, TypedDictIterableUnion, use_async) == { + "FOO": [{"fooBaz": "hello"}, {"fooBaz": "world"}] + } + + +@parametrize +@pytest.mark.asyncio +async def test_dictionary_items(use_async: bool) -> None: + class DictItems(TypedDict): + foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] + + assert await transform({"foo": {"foo_baz": "bar"}}, Dict[str, DictItems], use_async) == {"foo": {"fooBaz": "bar"}} + + +class TypedDictIterableUnionStr(TypedDict): + foo: Annotated[Union[str, Iterable[Baz8]], PropertyInfo(alias="FOO")] + + +@parametrize +@pytest.mark.asyncio +async def test_iterable_union_str(use_async: bool) -> None: + assert await transform({"foo": "bar"}, TypedDictIterableUnionStr, use_async) == {"FOO": "bar"} + assert cast(Any, await transform(iter([{"foo_baz": "bar"}]), Union[str, Iterable[Baz8]], use_async)) == [ + {"fooBaz": "bar"} + ] + + +class TypedDictBase64Input(TypedDict): + foo: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")] + + +@parametrize +@pytest.mark.asyncio +async def test_base64_file_input(use_async: bool) -> None: + # strings are left as-is + assert await transform({"foo": "bar"}, TypedDictBase64Input, use_async) == {"foo": "bar"} + + # pathlib.Path is automatically converted to base64 + assert await transform({"foo": SAMPLE_FILE_PATH}, TypedDictBase64Input, use_async) == { + "foo": "SGVsbG8sIHdvcmxkIQo=" + } # type: ignore[comparison-overlap] + + # io instances are automatically converted to base64 + assert await transform({"foo": io.StringIO("Hello, world!")}, TypedDictBase64Input, use_async) == { + "foo": "SGVsbG8sIHdvcmxkIQ==" + } # type: ignore[comparison-overlap] + assert await transform({"foo": io.BytesIO(b"Hello, world!")}, TypedDictBase64Input, use_async) == { + "foo": "SGVsbG8sIHdvcmxkIQ==" + } # type: ignore[comparison-overlap] + + +@parametrize +@pytest.mark.asyncio +async def test_transform_skipping(use_async: bool) -> None: + # lists of ints are left as-is + data = [1, 2, 3] + assert await transform(data, List[int], use_async) is data + + # iterables of ints are converted to a list + data = iter([1, 2, 3]) + assert await transform(data, Iterable[int], use_async) == [1, 2, 3] + + +@parametrize +@pytest.mark.asyncio +async def test_strips_notgiven(use_async: bool) -> None: + assert await transform({"foo_bar": "bar"}, Foo1, use_async) == {"fooBar": "bar"} + assert await transform({"foo_bar": not_given}, Foo1, use_async) == {} + + +@parametrize +@pytest.mark.asyncio +async def test_strips_omit(use_async: bool) -> None: + assert await transform({"foo_bar": "bar"}, Foo1, use_async) == {"fooBar": "bar"} + assert await transform({"foo_bar": omit}, Foo1, use_async) == {} diff --git a/tests/test_utils/test_datetime_parse.py b/tests/test_utils/test_datetime_parse.py new file mode 100644 index 000000000..f8eb5c9fe --- /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 runloop_api_client._utils import parse_date, parse_datetime + + +def create_tz(minutes: int) -> timezone: + return timezone(timedelta(minutes=minutes)) + + +@pytest.mark.parametrize( + "value,result", + [ + # Valid inputs + ("1494012444.883309", date(2017, 5, 5)), + (b"1494012444.883309", date(2017, 5, 5)), + (1_494_012_444.883_309, date(2017, 5, 5)), + ("1494012444", date(2017, 5, 5)), + (1_494_012_444, date(2017, 5, 5)), + (0, date(1970, 1, 1)), + ("2012-04-23", date(2012, 4, 23)), + (b"2012-04-23", date(2012, 4, 23)), + ("2012-4-9", date(2012, 4, 9)), + (date(2012, 4, 9), date(2012, 4, 9)), + (datetime(2012, 4, 9, 12, 15), date(2012, 4, 9)), + # Invalid inputs + ("x20120423", ValueError), + ("2012-04-56", ValueError), + (19_999_999_999, date(2603, 10, 11)), # just before watershed + (20_000_000_001, date(1970, 8, 20)), # just after watershed + (1_549_316_052, date(2019, 2, 4)), # nowish in s + (1_549_316_052_104, date(2019, 2, 4)), # nowish in ms + (1_549_316_052_104_324, date(2019, 2, 4)), # nowish in μs + (1_549_316_052_104_324_096, date(2019, 2, 4)), # nowish in ns + ("infinity", date(9999, 12, 31)), + ("inf", date(9999, 12, 31)), + (float("inf"), date(9999, 12, 31)), + ("infinity ", date(9999, 12, 31)), + (int("1" + "0" * 100), date(9999, 12, 31)), + (1e1000, date(9999, 12, 31)), + ("-infinity", date(1, 1, 1)), + ("-inf", date(1, 1, 1)), + ("nan", ValueError), + ], +) +def test_date_parsing(value: Union[str, bytes, int, float], result: Union[date, Type[Exception]]) -> None: + if type(result) == type and issubclass(result, Exception): # pyright: ignore[reportUnnecessaryIsInstance] + with pytest.raises(result): + parse_date(value) + else: + assert parse_date(value) == result + + +@pytest.mark.parametrize( + "value,result", + [ + # Valid inputs + # values in seconds + ("1494012444.883309", datetime(2017, 5, 5, 19, 27, 24, 883_309, tzinfo=timezone.utc)), + (1_494_012_444.883_309, datetime(2017, 5, 5, 19, 27, 24, 883_309, tzinfo=timezone.utc)), + ("1494012444", datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)), + (b"1494012444", datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)), + (1_494_012_444, datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)), + # values in ms + ("1494012444000.883309", datetime(2017, 5, 5, 19, 27, 24, 883, tzinfo=timezone.utc)), + ("-1494012444000.883309", datetime(1922, 8, 29, 4, 32, 35, 999117, tzinfo=timezone.utc)), + (1_494_012_444_000, datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)), + ("2012-04-23T09:15:00", datetime(2012, 4, 23, 9, 15)), + ("2012-4-9 4:8:16", datetime(2012, 4, 9, 4, 8, 16)), + ("2012-04-23T09:15:00Z", datetime(2012, 4, 23, 9, 15, 0, 0, timezone.utc)), + ("2012-4-9 4:8:16-0320", datetime(2012, 4, 9, 4, 8, 16, 0, create_tz(-200))), + ("2012-04-23T10:20:30.400+02:30", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(150))), + ("2012-04-23T10:20:30.400+02", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(120))), + ("2012-04-23T10:20:30.400-02", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(-120))), + (b"2012-04-23T10:20:30.400-02", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(-120))), + (datetime(2017, 5, 5), datetime(2017, 5, 5)), + (0, datetime(1970, 1, 1, 0, 0, 0, tzinfo=timezone.utc)), + # Invalid inputs + ("x20120423091500", ValueError), + ("2012-04-56T09:15:90", ValueError), + ("2012-04-23T11:05:00-25:00", ValueError), + (19_999_999_999, datetime(2603, 10, 11, 11, 33, 19, tzinfo=timezone.utc)), # just before watershed + (20_000_000_001, datetime(1970, 8, 20, 11, 33, 20, 1000, tzinfo=timezone.utc)), # just after watershed + (1_549_316_052, datetime(2019, 2, 4, 21, 34, 12, 0, tzinfo=timezone.utc)), # nowish in s + (1_549_316_052_104, datetime(2019, 2, 4, 21, 34, 12, 104_000, tzinfo=timezone.utc)), # nowish in ms + (1_549_316_052_104_324, datetime(2019, 2, 4, 21, 34, 12, 104_324, tzinfo=timezone.utc)), # nowish in μs + (1_549_316_052_104_324_096, datetime(2019, 2, 4, 21, 34, 12, 104_324, tzinfo=timezone.utc)), # nowish in ns + ("infinity", datetime(9999, 12, 31, 23, 59, 59, 999999)), + ("inf", datetime(9999, 12, 31, 23, 59, 59, 999999)), + ("inf ", datetime(9999, 12, 31, 23, 59, 59, 999999)), + (1e50, datetime(9999, 12, 31, 23, 59, 59, 999999)), + (float("inf"), datetime(9999, 12, 31, 23, 59, 59, 999999)), + ("-infinity", datetime(1, 1, 1, 0, 0)), + ("-inf", datetime(1, 1, 1, 0, 0)), + ("nan", ValueError), + ], +) +def test_datetime_parsing(value: Union[str, bytes, int, float], result: Union[datetime, Type[Exception]]) -> None: + if type(result) == type and issubclass(result, Exception): # pyright: ignore[reportUnnecessaryIsInstance] + with pytest.raises(result): + parse_datetime(value) + else: + assert parse_datetime(value) == result diff --git a/tests/test_utils/test_json.py b/tests/test_utils/test_json.py new file mode 100644 index 000000000..7e9390783 --- /dev/null +++ b/tests/test_utils/test_json.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +import datetime +from typing import Union + +import pydantic + +from runloop_api_client import _compat +from runloop_api_client._utils._json import openapi_dumps + + +class TestOpenapiDumps: + def test_basic(self) -> None: + data = {"key": "value", "number": 42} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"key":"value","number":42}' + + def test_datetime_serialization(self) -> None: + dt = datetime.datetime(2023, 1, 1, 12, 0, 0) + data = {"datetime": dt} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"datetime":"2023-01-01T12:00:00"}' + + def test_pydantic_model_serialization(self) -> None: + class User(pydantic.BaseModel): + first_name: str + last_name: str + age: int + + model_instance = User(first_name="John", last_name="Kramer", age=83) + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"first_name":"John","last_name":"Kramer","age":83}}' + + def test_pydantic_model_with_default_values(self) -> None: + class User(pydantic.BaseModel): + name: str + role: str = "user" + active: bool = True + score: int = 0 + + model_instance = User(name="Alice") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Alice"}}' + + def test_pydantic_model_with_default_values_overridden(self) -> None: + class User(pydantic.BaseModel): + name: str + role: str = "user" + active: bool = True + + model_instance = User(name="Bob", role="admin", active=False) + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Bob","role":"admin","active":false}}' + + def test_pydantic_model_with_alias(self) -> None: + class User(pydantic.BaseModel): + first_name: str = pydantic.Field(alias="firstName") + last_name: str = pydantic.Field(alias="lastName") + + model_instance = User(firstName="John", lastName="Doe") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"firstName":"John","lastName":"Doe"}}' + + def test_pydantic_model_with_alias_and_default(self) -> None: + class User(pydantic.BaseModel): + user_name: str = pydantic.Field(alias="userName") + user_role: str = pydantic.Field(default="member", alias="userRole") + is_active: bool = pydantic.Field(default=True, alias="isActive") + + model_instance = User(userName="charlie") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"userName":"charlie"}}' + + model_with_overrides = User(userName="diana", userRole="admin", isActive=False) + data = {"model": model_with_overrides} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"userName":"diana","userRole":"admin","isActive":false}}' + + def test_pydantic_model_with_nested_models_and_defaults(self) -> None: + class Address(pydantic.BaseModel): + street: str + city: str = "Unknown" + + class User(pydantic.BaseModel): + name: str + address: Address + verified: bool = False + + if _compat.PYDANTIC_V1: + # to handle forward references in Pydantic v1 + User.update_forward_refs(**locals()) # type: ignore[reportDeprecated] + + address = Address(street="123 Main St") + user = User(name="Diana", address=address) + data = {"user": user} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"user":{"name":"Diana","address":{"street":"123 Main St"}}}' + + address_with_city = Address(street="456 Oak Ave", city="Boston") + user_verified = User(name="Eve", address=address_with_city, verified=True) + data = {"user": user_verified} + json_bytes = openapi_dumps(data) + assert ( + json_bytes == b'{"user":{"name":"Eve","address":{"street":"456 Oak Ave","city":"Boston"},"verified":true}}' + ) + + def test_pydantic_model_with_optional_fields(self) -> None: + class User(pydantic.BaseModel): + name: str + email: Union[str, None] + phone: Union[str, None] + + model_with_none = User(name="Eve", email=None, phone=None) + data = {"model": model_with_none} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Eve","email":null,"phone":null}}' + + model_with_values = User(name="Frank", email="frank@example.com", phone=None) + data = {"model": model_with_values} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Frank","email":"frank@example.com","phone":null}}' diff --git a/tests/test_utils/test_proxy.py b/tests/test_utils/test_proxy.py new file mode 100644 index 000000000..e3506d9c3 --- /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 runloop_api_client._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 000000000..1b58221e7 --- /dev/null +++ b/tests/test_utils/test_typing.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +from typing import Generic, TypeVar, cast + +from runloop_api_client._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 000000000..606067e05 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,167 @@ +from __future__ import annotations + +import os +import inspect +import traceback +import contextlib +from typing import Any, TypeVar, Iterator, Sequence, cast +from datetime import date, datetime +from typing_extensions import Literal, get_args, get_origin, assert_type + +from runloop_api_client._types import Omit, NoneType +from runloop_api_client._utils import ( + is_dict, + is_list, + is_list_type, + is_union_type, + extract_type_arg, + is_sequence_type, + is_annotated_type, + is_type_alias_type, +) +from runloop_api_client._compat import PYDANTIC_V1, field_outer_type, get_model_fields +from runloop_api_client._models import BaseModel + +BaseModelT = TypeVar("BaseModelT", bound=BaseModel) + + +def assert_matches_model(model: type[BaseModelT], value: BaseModelT, *, path: list[str]) -> bool: + for name, field in get_model_fields(model).items(): + field_value = getattr(value, name) + if PYDANTIC_V1: + # in v1 nullability was structured differently + # https://docs.pydantic.dev/2.0/migration/#required-optional-and-nullable-fields + allow_none = getattr(field, "allow_none", False) + else: + allow_none = False + + assert_matches_type( + field_outer_type(field), + field_value, + path=[*path, name], + allow_none=allow_none, + ) + + return True + + +# Note: the `path` argument is only used to improve error messages when `--showlocals` is used +def assert_matches_type( + type_: Any, + value: object, + *, + path: list[str], + allow_none: bool = False, +) -> None: + if is_type_alias_type(type_): + type_ = type_.__value__ + + # unwrap `Annotated[T, ...]` -> `T` + if is_annotated_type(type_): + type_ = extract_type_arg(type_, 0) + + if allow_none and value is None: + return + + if type_ is None or type_ is NoneType: + assert value is None + return + + origin = get_origin(type_) or type_ + + if is_list_type(type_): + return _assert_list_type(type_, value) + + if is_sequence_type(type_): + assert isinstance(value, Sequence) + inner_type = get_args(type_)[0] + for entry in value: # type: ignore + assert_type(inner_type, entry) # type: ignore + return + + if origin == str: + assert isinstance(value, str) + elif origin == int: + assert isinstance(value, int) + elif origin == bool: + assert isinstance(value, bool) + elif origin == float: + assert isinstance(value, float) + elif origin == bytes: + assert isinstance(value, bytes) + elif origin == datetime: + assert isinstance(value, datetime) + elif origin == date: + assert isinstance(value, date) + elif origin == object: + # nothing to do here, the expected type is unknown + pass + elif origin == Literal: + assert value in get_args(type_) + elif origin == dict: + assert is_dict(value) + + args = get_args(type_) + key_type = args[0] + items_type = args[1] + + for key, item in value.items(): + assert_matches_type(key_type, key, path=[*path, ""]) + assert_matches_type(items_type, item, path=[*path, ""]) + elif is_union_type(type_): + variants = get_args(type_) + + try: + none_index = variants.index(type(None)) + except ValueError: + pass + else: + # special case Optional[T] for better error messages + if len(variants) == 2: + if value is None: + # valid + return + + return assert_matches_type(type_=variants[not none_index], value=value, path=path) + + for i, variant in enumerate(variants): + try: + assert_matches_type(variant, value, path=[*path, f"variant {i}"]) + return + except AssertionError: + traceback.print_exc() + continue + + raise AssertionError("Did not match any variants") + elif issubclass(origin, BaseModel): + assert isinstance(value, type_) + assert assert_matches_model(type_, cast(Any, value), path=path) + elif inspect.isclass(origin) and origin.__name__ == "HttpxBinaryResponseContent": + assert value.__class__.__name__ == "HttpxBinaryResponseContent" + else: + assert None, f"Unhandled field type: {type_}" + + +def _assert_list_type(type_: type[object], value: object) -> None: + assert is_list(value) + + inner_type = get_args(type_)[0] + for entry in value: + assert_type(inner_type, entry) # type: ignore + + +@contextlib.contextmanager +def update_env(**new_env: str | Omit) -> Iterator[None]: + old = os.environ.copy() + + try: + for name, value in new_env.items(): + if isinstance(value, Omit): + os.environ.pop(name, None) + else: + os.environ[name] = value + + yield None + finally: + os.environ.clear() + os.environ.update(old) diff --git a/uv.lock b/uv.lock new file mode 100644 index 000000000..77d91be5f --- /dev/null +++ b/uv.lock @@ -0,0 +1,1829 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2'", + "python_full_version < '3.10' and extra != 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-18-runloop-api-client-pydantic-v1' and extra != 'group-18-runloop-api-client-pydantic-v2'", + "python_full_version < '3.10' and extra == 'group-18-runloop-api-client-pydantic-v1' and extra != 'group-18-runloop-api-client-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-18-runloop-api-client-pydantic-v1' and extra != 'group-18-runloop-api-client-pydantic-v2'", + "python_full_version < '3.10' and extra != 'group-18-runloop-api-client-pydantic-v1' and extra != 'group-18-runloop-api-client-pydantic-v2'", +] +conflicts = [[ + { package = "runloop-api-client", group = "pydantic-v1" }, + { package = "runloop-api-client", group = "pydantic-v2" }, +]] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "async-timeout", marker = "python_full_version < '3.11' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/d6/5aec9313ee6ea9c7cde8b891b69f4ff4001416867104580670a31daeba5b/aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7", size = 738950, upload-time = "2026-01-03T17:29:13.002Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/8fa90a7e6d11ff20a18837a8e2b5dd23db01aabc475aa9271c8ad33299f5/aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821", size = 496099, upload-time = "2026-01-03T17:29:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/d2/23/b81f744d402510a8366b74eb420fc0cc1170d0c43daca12d10814df85f10/aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845", size = 491072, upload-time = "2026-01-03T17:29:16.922Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e1/56d1d1c0dd334cd203dd97706ce004c1aa24b34a813b0b8daf3383039706/aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af", size = 1671588, upload-time = "2026-01-03T17:29:18.539Z" }, + { url = "https://files.pythonhosted.org/packages/5f/34/8d7f962604f4bc2b4e39eb1220dac7d4e4cba91fb9ba0474b4ecd67db165/aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940", size = 1640334, upload-time = "2026-01-03T17:29:21.028Z" }, + { url = "https://files.pythonhosted.org/packages/94/1d/fcccf2c668d87337ddeef9881537baee13c58d8f01f12ba8a24215f2b804/aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160", size = 1722656, upload-time = "2026-01-03T17:29:22.531Z" }, + { url = "https://files.pythonhosted.org/packages/aa/98/c6f3b081c4c606bc1e5f2ec102e87d6411c73a9ef3616fea6f2d5c98c062/aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7", size = 1817625, upload-time = "2026-01-03T17:29:24.276Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c0/cfcc3d2e11b477f86e1af2863f3858c8850d751ce8dc39c4058a072c9e54/aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455", size = 1672604, upload-time = "2026-01-03T17:29:26.099Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/6b4ffcbcac4c6a5d041343a756f34a6dd26174ae07f977a64fe028dda5b0/aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279", size = 1554370, upload-time = "2026-01-03T17:29:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f0/e3ddfa93f17d689dbe014ba048f18e0c9f9b456033b70e94349a2e9048be/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e", size = 1642023, upload-time = "2026-01-03T17:29:30.002Z" }, + { url = "https://files.pythonhosted.org/packages/eb/45/c14019c9ec60a8e243d06d601b33dcc4fd92379424bde3021725859d7f99/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d", size = 1649680, upload-time = "2026-01-03T17:29:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fd/09c9451dae5aa5c5ed756df95ff9ef549d45d4be663bafd1e4954fd836f0/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808", size = 1692407, upload-time = "2026-01-03T17:29:33.392Z" }, + { url = "https://files.pythonhosted.org/packages/a6/81/938bc2ec33c10efd6637ccb3d22f9f3160d08e8f3aa2587a2c2d5ab578eb/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40", size = 1543047, upload-time = "2026-01-03T17:29:34.855Z" }, + { url = "https://files.pythonhosted.org/packages/f7/23/80488ee21c8d567c83045e412e1d9b7077d27171591a4eb7822586e8c06a/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29", size = 1715264, upload-time = "2026-01-03T17:29:36.389Z" }, + { url = "https://files.pythonhosted.org/packages/e2/83/259a8da6683182768200b368120ab3deff5370bed93880fb9a3a86299f34/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11", size = 1657275, upload-time = "2026-01-03T17:29:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/3f/4f/2c41f800a0b560785c10fb316216ac058c105f9be50bdc6a285de88db625/aiohttp-3.13.3-cp310-cp310-win32.whl", hash = "sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd", size = 434053, upload-time = "2026-01-03T17:29:40.074Z" }, + { url = "https://files.pythonhosted.org/packages/80/df/29cd63c7ecfdb65ccc12f7d808cac4fa2a19544660c06c61a4a48462de0c/aiohttp-3.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c", size = 456687, upload-time = "2026-01-03T17:29:41.819Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051, upload-time = "2026-01-03T17:29:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234, upload-time = "2026-01-03T17:29:44.822Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979, upload-time = "2026-01-03T17:29:46.405Z" }, + { url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297, upload-time = "2026-01-03T17:29:48.083Z" }, + { url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172, upload-time = "2026-01-03T17:29:49.648Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405, upload-time = "2026-01-03T17:29:51.244Z" }, + { url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449, upload-time = "2026-01-03T17:29:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444, upload-time = "2026-01-03T17:29:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038, upload-time = "2026-01-03T17:29:57.179Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156, upload-time = "2026-01-03T17:29:58.914Z" }, + { url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340, upload-time = "2026-01-03T17:30:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041, upload-time = "2026-01-03T17:30:03.609Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024, upload-time = "2026-01-03T17:30:05.132Z" }, + { url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590, upload-time = "2026-01-03T17:30:07.135Z" }, + { url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355, upload-time = "2026-01-03T17:30:09.083Z" }, + { url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701, upload-time = "2026-01-03T17:30:10.869Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678, upload-time = "2026-01-03T17:30:12.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, + { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, + { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, + { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, + { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, + { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, + { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, + { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, + { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, + { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, + { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, + { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, + { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, + { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, + { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, + { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, + { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, + { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, + { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, + { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" }, + { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, + { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, + { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, + { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, + { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, + { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, + { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, + { url = "https://files.pythonhosted.org/packages/bf/79/446655656861d3e7e2c32bfcf160c7aa9e9dc63776a691b124dba65cdd77/aiohttp-3.13.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31a83ea4aead760dfcb6962efb1d861db48c34379f2ff72db9ddddd4cda9ea2e", size = 741433, upload-time = "2026-01-03T17:32:26.453Z" }, + { url = "https://files.pythonhosted.org/packages/cb/49/773c4b310b5140d2fb5e79bb0bf40b7b41dad80a288ca1a8759f5f72bda9/aiohttp-3.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:988a8c5e317544fdf0d39871559e67b6341065b87fceac641108c2096d5506b7", size = 497332, upload-time = "2026-01-03T17:32:28.37Z" }, + { url = "https://files.pythonhosted.org/packages/bc/31/1dcbc4b83a4e6f76a0ad883f07f21ffbfe29750c89db97381701508c9f45/aiohttp-3.13.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9b174f267b5cfb9a7dba9ee6859cecd234e9a681841eb85068059bc867fb8f02", size = 492365, upload-time = "2026-01-03T17:32:30.234Z" }, + { url = "https://files.pythonhosted.org/packages/5a/b5/b50657496c8754482cd7964e50aaf3aa84b3db61ed45daec4c1aec5b94b4/aiohttp-3.13.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:947c26539750deeaee933b000fb6517cc770bbd064bad6033f1cff4803881e43", size = 1660440, upload-time = "2026-01-03T17:32:32.586Z" }, + { url = "https://files.pythonhosted.org/packages/2a/73/9b69e5139d89d75127569298931444ad78ea86a5befd5599780b1e9a6880/aiohttp-3.13.3-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9ebf57d09e131f5323464bd347135a88622d1c0976e88ce15b670e7ad57e4bd6", size = 1632740, upload-time = "2026-01-03T17:32:34.793Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fe/3ea9b5af694b4e3aec0d0613a806132ca744747146fca68e96bf056f61a7/aiohttp-3.13.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4ae5b5a0e1926e504c81c5b84353e7a5516d8778fbbff00429fe7b05bb25cbce", size = 1719782, upload-time = "2026-01-03T17:32:37.737Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c2/46b3b06e60851cbb71efb0f79a3267279cbef7b12c58e68a1e897f269cca/aiohttp-3.13.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ba0eea45eb5cc3172dbfc497c066f19c41bac70963ea1a67d51fc92e4cf9a80", size = 1813527, upload-time = "2026-01-03T17:32:39.973Z" }, + { url = "https://files.pythonhosted.org/packages/36/23/71ceb78c769ed65fe4c697692de232b63dab399210678d2b00961ccb0619/aiohttp-3.13.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bae5c2ed2eae26cc382020edad80d01f36cb8e746da40b292e68fec40421dc6a", size = 1661268, upload-time = "2026-01-03T17:32:42.082Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8d/86e929523d955e85ebab7c0e2b9e0cb63604cfc27dc3280e10d0063cf682/aiohttp-3.13.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a60e60746623925eab7d25823329941aee7242d559baa119ca2b253c88a7bd6", size = 1552742, upload-time = "2026-01-03T17:32:44.622Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/3f5987cba1bab6bd151f0d97aa60f0ce04d3c83316692a6bb6ba2fb69f92/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e50a2e1404f063427c9d027378472316201a2290959a295169bcf25992d04558", size = 1632918, upload-time = "2026-01-03T17:32:46.749Z" }, + { url = "https://files.pythonhosted.org/packages/be/2c/7e1e85121f2e31ee938cb83a8f32dfafd4908530c10fabd6d46761c12ac7/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:9a9dc347e5a3dc7dfdbc1f82da0ef29e388ddb2ed281bfce9dd8248a313e62b7", size = 1644446, upload-time = "2026-01-03T17:32:49.063Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/ce6133d423ad0e8ca976a7c848f7146bca3520eea4ccf6b95e2d077c9d20/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b46020d11d23fe16551466c77823df9cc2f2c1e63cc965daf67fa5eec6ca1877", size = 1689487, upload-time = "2026-01-03T17:32:51.113Z" }, + { url = "https://files.pythonhosted.org/packages/50/f7/ff7a27c15603d460fd1366b3c22054f7ae4fa9310aca40b43bde35867fcd/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:69c56fbc1993fa17043e24a546959c0178fe2b5782405ad4559e6c13975c15e3", size = 1540715, upload-time = "2026-01-03T17:32:53.38Z" }, + { url = "https://files.pythonhosted.org/packages/17/02/053f11346e5b962e6d8a1c4f8c70c29d5970a1b4b8e7894c68e12c27a57f/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:b99281b0704c103d4e11e72a76f1b543d4946fea7dd10767e7e1b5f00d4e5704", size = 1711835, upload-time = "2026-01-03T17:32:56.088Z" }, + { url = "https://files.pythonhosted.org/packages/fb/71/9b9761ddf276fd6708d13720197cbac19b8d67ecfa9116777924056cfcaa/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:40c5e40ecc29ba010656c18052b877a1c28f84344825efa106705e835c28530f", size = 1649593, upload-time = "2026-01-03T17:32:58.181Z" }, + { url = "https://files.pythonhosted.org/packages/ae/72/5d817e9ea218acae12a5e3b9ad1178cf0c12fc3570c0b47eea2daf95f9ea/aiohttp-3.13.3-cp39-cp39-win32.whl", hash = "sha256:56339a36b9f1fc708260c76c87e593e2afb30d26de9ae1eb445b5e051b98a7a1", size = 434831, upload-time = "2026-01-03T17:33:00.577Z" }, + { url = "https://files.pythonhosted.org/packages/39/cb/22659d9bf3149b7a2927bc2769cc9c8f8f5a80eba098398e03c199a43a85/aiohttp-3.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:c6b8568a3bb5819a0ad087f16d40e5a3fb6099f39ea1d5625a3edc1e923fc538", size = 457697, upload-time = "2026-01-03T17:33:03.167Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "dirty-equals" +version = "0.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/1d/c5913ac9d6615515a00f4bdc71356d302437cb74ff2e9aaccd3c14493b78/dirty_equals-0.11.tar.gz", hash = "sha256:f4ac74ee88f2d11e2fa0f65eb30ee4f07105c5f86f4dc92b09eb1138775027c3", size = 128067, upload-time = "2025-11-17T01:51:24.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/8d/dbff05239043271dbeace563a7686212a3dd517864a35623fe4d4a64ca19/dirty_equals-0.11-py3-none-any.whl", hash = "sha256:b1d7093273fc2f9be12f443a8ead954ef6daaf6746fd42ef3a5616433ee85286", size = 28051, upload-time = "2025-11-17T01:51:22.849Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230, upload-time = "2025-10-06T05:35:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621, upload-time = "2025-10-06T05:35:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889, upload-time = "2025-10-06T05:35:26.797Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464, upload-time = "2025-10-06T05:35:28.254Z" }, + { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649, upload-time = "2025-10-06T05:35:29.454Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188, upload-time = "2025-10-06T05:35:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748, upload-time = "2025-10-06T05:35:32.101Z" }, + { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351, upload-time = "2025-10-06T05:35:33.834Z" }, + { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767, upload-time = "2025-10-06T05:35:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887, upload-time = "2025-10-06T05:35:36.354Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785, upload-time = "2025-10-06T05:35:37.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312, upload-time = "2025-10-06T05:35:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650, upload-time = "2025-10-06T05:35:40.377Z" }, + { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659, upload-time = "2025-10-06T05:35:41.863Z" }, + { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837, upload-time = "2025-10-06T05:35:43.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989, upload-time = "2025-10-06T05:35:44.596Z" }, + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/c2/59/ae5cdac87a00962122ea37bb346d41b66aec05f9ce328fa2b9e216f8967b/frozenlist-1.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47", size = 86967, upload-time = "2025-10-06T05:37:55.607Z" }, + { url = "https://files.pythonhosted.org/packages/8a/10/17059b2db5a032fd9323c41c39e9d1f5f9d0c8f04d1e4e3e788573086e61/frozenlist-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca", size = 49984, upload-time = "2025-10-06T05:37:57.049Z" }, + { url = "https://files.pythonhosted.org/packages/4b/de/ad9d82ca8e5fa8f0c636e64606553c79e2b859ad253030b62a21fe9986f5/frozenlist-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068", size = 50240, upload-time = "2025-10-06T05:37:58.145Z" }, + { url = "https://files.pythonhosted.org/packages/4e/45/3dfb7767c2a67d123650122b62ce13c731b6c745bc14424eea67678b508c/frozenlist-1.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95", size = 219472, upload-time = "2025-10-06T05:37:59.239Z" }, + { url = "https://files.pythonhosted.org/packages/0b/bf/5bf23d913a741b960d5c1dac7c1985d8a2a1d015772b2d18ea168b08e7ff/frozenlist-1.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459", size = 221531, upload-time = "2025-10-06T05:38:00.521Z" }, + { url = "https://files.pythonhosted.org/packages/d0/03/27ec393f3b55860859f4b74cdc8c2a4af3dbf3533305e8eacf48a4fd9a54/frozenlist-1.8.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675", size = 219211, upload-time = "2025-10-06T05:38:01.842Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ad/0fd00c404fa73fe9b169429e9a972d5ed807973c40ab6b3cf9365a33d360/frozenlist-1.8.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61", size = 231775, upload-time = "2025-10-06T05:38:03.384Z" }, + { url = "https://files.pythonhosted.org/packages/8a/c3/86962566154cb4d2995358bc8331bfc4ea19d07db1a96f64935a1607f2b6/frozenlist-1.8.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6", size = 236631, upload-time = "2025-10-06T05:38:04.609Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/6ffad161dbd83782d2c66dc4d378a9103b31770cb1e67febf43aea42d202/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5", size = 218632, upload-time = "2025-10-06T05:38:05.917Z" }, + { url = "https://files.pythonhosted.org/packages/58/b2/4677eee46e0a97f9b30735e6ad0bf6aba3e497986066eb68807ac85cf60f/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3", size = 235967, upload-time = "2025-10-06T05:38:07.614Z" }, + { url = "https://files.pythonhosted.org/packages/05/f3/86e75f8639c5a93745ca7addbbc9de6af56aebb930d233512b17e46f6493/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1", size = 228799, upload-time = "2025-10-06T05:38:08.845Z" }, + { url = "https://files.pythonhosted.org/packages/30/00/39aad3a7f0d98f5eb1d99a3c311215674ed87061aecee7851974b335c050/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178", size = 230566, upload-time = "2025-10-06T05:38:10.52Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4d/aa144cac44568d137846ddc4d5210fb5d9719eb1d7ec6fa2728a54b5b94a/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda", size = 217715, upload-time = "2025-10-06T05:38:11.832Z" }, + { url = "https://files.pythonhosted.org/packages/64/4c/8f665921667509d25a0dd72540513bc86b356c95541686f6442a3283019f/frozenlist-1.8.0-cp39-cp39-win32.whl", hash = "sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087", size = 39933, upload-time = "2025-10-06T05:38:13.061Z" }, + { url = "https://files.pythonhosted.org/packages/79/bd/bcc926f87027fad5e59926ff12d136e1082a115025d33c032d1cd69ab377/frozenlist-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a", size = 44121, upload-time = "2025-10-06T05:38:14.572Z" }, + { url = "https://files.pythonhosted.org/packages/4c/07/9c2e4eb7584af4b705237b971b89a4155a8e57599c4483a131a39256a9a0/frozenlist-1.8.0-cp39-cp39-win_arm64.whl", hash = "sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103", size = 40312, upload-time = "2025-10-06T05:38:15.699Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-aiohttp" +version = "0.1.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/2c/b894861cecf030fb45675ea24aa55b5722e97c602a163d872fca66c5a6d8/httpx_aiohttp-0.1.12.tar.gz", hash = "sha256:81feec51fd82c0ecfa0e9aaf1b1a6c2591260d5e2bcbeb7eb0277a78e610df2c", size = 275945, upload-time = "2025-12-12T10:12:15.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/8d/85c9701e9af72ca132a1783e2a54364a90c6da832304416a30fc11196ab2/httpx_aiohttp-0.1.12-py3-none-any.whl", hash = "sha256:5b0eac39a7f360fa7867a60bcb46bb1024eada9c01cbfecdb54dc1edb3fb7141", size = 6367, upload-time = "2025-12-12T10:12:14.018Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-18-runloop-api-client-pydantic-v1' and extra != 'group-18-runloop-api-client-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-18-runloop-api-client-pydantic-v1' and extra != 'group-18-runloop-api-client-pydantic-v2'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version < '3.10' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-18-runloop-api-client-pydantic-v1' and extra != 'group-18-runloop-api-client-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-18-runloop-api-client-pydantic-v1' and extra != 'group-18-runloop-api-client-pydantic-v2'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version >= '3.10' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/63/7bdd4adc330abcca54c85728db2327130e49e52e8c3ce685cec44e0f2e9f/multidict-6.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9f474ad5acda359c8758c8accc22032c6abe6dc87a8be2440d097785e27a9349", size = 77153, upload-time = "2025-10-06T14:48:26.409Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bb/b6c35ff175ed1a3142222b78455ee31be71a8396ed3ab5280fbe3ebe4e85/multidict-6.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b7a9db5a870f780220e931d0002bbfd88fb53aceb6293251e2c839415c1b20e", size = 44993, upload-time = "2025-10-06T14:48:28.4Z" }, + { url = "https://files.pythonhosted.org/packages/e0/1f/064c77877c5fa6df6d346e68075c0f6998547afe952d6471b4c5f6a7345d/multidict-6.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03ca744319864e92721195fa28c7a3b2bc7b686246b35e4078c1e4d0eb5466d3", size = 44607, upload-time = "2025-10-06T14:48:29.581Z" }, + { url = "https://files.pythonhosted.org/packages/04/7a/bf6aa92065dd47f287690000b3d7d332edfccb2277634cadf6a810463c6a/multidict-6.7.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f0e77e3c0008bc9316e662624535b88d360c3a5d3f81e15cf12c139a75250046", size = 241847, upload-time = "2025-10-06T14:48:32.107Z" }, + { url = "https://files.pythonhosted.org/packages/94/39/297a8de920f76eda343e4ce05f3b489f0ab3f9504f2576dfb37b7c08ca08/multidict-6.7.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08325c9e5367aa379a3496aa9a022fe8837ff22e00b94db256d3a1378c76ab32", size = 242616, upload-time = "2025-10-06T14:48:34.054Z" }, + { url = "https://files.pythonhosted.org/packages/39/3a/d0eee2898cfd9d654aea6cb8c4addc2f9756e9a7e09391cfe55541f917f7/multidict-6.7.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e2862408c99f84aa571ab462d25236ef9cb12a602ea959ba9c9009a54902fc73", size = 222333, upload-time = "2025-10-06T14:48:35.9Z" }, + { url = "https://files.pythonhosted.org/packages/05/48/3b328851193c7a4240815b71eea165b49248867bbb6153a0aee227a0bb47/multidict-6.7.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d72a9a2d885f5c208b0cb91ff2ed43636bb7e345ec839ff64708e04f69a13cc", size = 253239, upload-time = "2025-10-06T14:48:37.302Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ca/0706a98c8d126a89245413225ca4a3fefc8435014de309cf8b30acb68841/multidict-6.7.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:478cc36476687bac1514d651cbbaa94b86b0732fb6855c60c673794c7dd2da62", size = 251618, upload-time = "2025-10-06T14:48:38.963Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4f/9c7992f245554d8b173f6f0a048ad24b3e645d883f096857ec2c0822b8bd/multidict-6.7.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6843b28b0364dc605f21481c90fadb5f60d9123b442eb8a726bb74feef588a84", size = 241655, upload-time = "2025-10-06T14:48:40.312Z" }, + { url = "https://files.pythonhosted.org/packages/31/79/26a85991ae67efd1c0b1fc2e0c275b8a6aceeb155a68861f63f87a798f16/multidict-6.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23bfeee5316266e5ee2d625df2d2c602b829435fc3a235c2ba2131495706e4a0", size = 239245, upload-time = "2025-10-06T14:48:41.848Z" }, + { url = "https://files.pythonhosted.org/packages/14/1e/75fa96394478930b79d0302eaf9a6c69f34005a1a5251ac8b9c336486ec9/multidict-6.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:680878b9f3d45c31e1f730eef731f9b0bc1da456155688c6745ee84eb818e90e", size = 233523, upload-time = "2025-10-06T14:48:43.749Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5e/085544cb9f9c4ad2b5d97467c15f856df8d9bac410cffd5c43991a5d878b/multidict-6.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:eb866162ef2f45063acc7a53a88ef6fe8bf121d45c30ea3c9cd87ce7e191a8d4", size = 243129, upload-time = "2025-10-06T14:48:45.225Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c3/e9d9e2f20c9474e7a8fcef28f863c5cbd29bb5adce6b70cebe8bdad0039d/multidict-6.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:df0e3bf7993bdbeca5ac25aa859cf40d39019e015c9c91809ba7093967f7a648", size = 248999, upload-time = "2025-10-06T14:48:46.703Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3f/df171b6efa3239ae33b97b887e42671cd1d94d460614bfb2c30ffdab3b95/multidict-6.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:661709cdcd919a2ece2234f9bae7174e5220c80b034585d7d8a755632d3e2111", size = 243711, upload-time = "2025-10-06T14:48:48.146Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2f/9b5564888c4e14b9af64c54acf149263721a283aaf4aa0ae89b091d5d8c1/multidict-6.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:096f52730c3fb8ed419db2d44391932b63891b2c5ed14850a7e215c0ba9ade36", size = 237504, upload-time = "2025-10-06T14:48:49.447Z" }, + { url = "https://files.pythonhosted.org/packages/6c/3a/0bd6ca0f7d96d790542d591c8c3354c1e1b6bfd2024d4d92dc3d87485ec7/multidict-6.7.0-cp310-cp310-win32.whl", hash = "sha256:afa8a2978ec65d2336305550535c9c4ff50ee527914328c8677b3973ade52b85", size = 41422, upload-time = "2025-10-06T14:48:50.789Z" }, + { url = "https://files.pythonhosted.org/packages/00/35/f6a637ea2c75f0d3b7c7d41b1189189acff0d9deeb8b8f35536bb30f5e33/multidict-6.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7", size = 46050, upload-time = "2025-10-06T14:48:51.938Z" }, + { url = "https://files.pythonhosted.org/packages/e7/b8/f7bf8329b39893d02d9d95cf610c75885d12fc0f402b1c894e1c8e01c916/multidict-6.7.0-cp310-cp310-win_arm64.whl", hash = "sha256:4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0", size = 43153, upload-time = "2025-10-06T14:48:53.146Z" }, + { url = "https://files.pythonhosted.org/packages/34/9e/5c727587644d67b2ed479041e4b1c58e30afc011e3d45d25bbe35781217c/multidict-6.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc", size = 76604, upload-time = "2025-10-06T14:48:54.277Z" }, + { url = "https://files.pythonhosted.org/packages/17/e4/67b5c27bd17c085a5ea8f1ec05b8a3e5cba0ca734bfcad5560fb129e70ca/multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721", size = 44715, upload-time = "2025-10-06T14:48:55.445Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/866a5d77be6ea435711bef2a4291eed11032679b6b28b56b4776ab06ba3e/multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6", size = 44332, upload-time = "2025-10-06T14:48:56.706Z" }, + { url = "https://files.pythonhosted.org/packages/31/61/0c2d50241ada71ff61a79518db85ada85fdabfcf395d5968dae1cbda04e5/multidict-6.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c", size = 245212, upload-time = "2025-10-06T14:48:58.042Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e0/919666a4e4b57fff1b57f279be1c9316e6cdc5de8a8b525d76f6598fefc7/multidict-6.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7", size = 246671, upload-time = "2025-10-06T14:49:00.004Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cc/d027d9c5a520f3321b65adea289b965e7bcbd2c34402663f482648c716ce/multidict-6.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7", size = 225491, upload-time = "2025-10-06T14:49:01.393Z" }, + { url = "https://files.pythonhosted.org/packages/75/c4/bbd633980ce6155a28ff04e6a6492dd3335858394d7bb752d8b108708558/multidict-6.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9", size = 257322, upload-time = "2025-10-06T14:49:02.745Z" }, + { url = "https://files.pythonhosted.org/packages/4c/6d/d622322d344f1f053eae47e033b0b3f965af01212de21b10bcf91be991fb/multidict-6.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8", size = 254694, upload-time = "2025-10-06T14:49:04.15Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9f/78f8761c2705d4c6d7516faed63c0ebdac569f6db1bef95e0d5218fdc146/multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd", size = 246715, upload-time = "2025-10-06T14:49:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/950818e04f91b9c2b95aab3d923d9eabd01689d0dcd889563988e9ea0fd8/multidict-6.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb", size = 243189, upload-time = "2025-10-06T14:49:07.37Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3d/77c79e1934cad2ee74991840f8a0110966d9599b3af95964c0cd79bb905b/multidict-6.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6", size = 237845, upload-time = "2025-10-06T14:49:08.759Z" }, + { url = "https://files.pythonhosted.org/packages/63/1b/834ce32a0a97a3b70f86437f685f880136677ac00d8bce0027e9fd9c2db7/multidict-6.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2", size = 246374, upload-time = "2025-10-06T14:49:10.574Z" }, + { url = "https://files.pythonhosted.org/packages/23/ef/43d1c3ba205b5dec93dc97f3fba179dfa47910fc73aaaea4f7ceb41cec2a/multidict-6.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff", size = 253345, upload-time = "2025-10-06T14:49:12.331Z" }, + { url = "https://files.pythonhosted.org/packages/6b/03/eaf95bcc2d19ead522001f6a650ef32811aa9e3624ff0ad37c445c7a588c/multidict-6.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b", size = 246940, upload-time = "2025-10-06T14:49:13.821Z" }, + { url = "https://files.pythonhosted.org/packages/e8/df/ec8a5fd66ea6cd6f525b1fcbb23511b033c3e9bc42b81384834ffa484a62/multidict-6.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34", size = 242229, upload-time = "2025-10-06T14:49:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a2/59b405d59fd39ec86d1142630e9049243015a5f5291ba49cadf3c090c541/multidict-6.7.0-cp311-cp311-win32.whl", hash = "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff", size = 41308, upload-time = "2025-10-06T14:49:16.871Z" }, + { url = "https://files.pythonhosted.org/packages/32/0f/13228f26f8b882c34da36efa776c3b7348455ec383bab4a66390e42963ae/multidict-6.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81", size = 46037, upload-time = "2025-10-06T14:49:18.457Z" }, + { url = "https://files.pythonhosted.org/packages/84/1f/68588e31b000535a3207fd3c909ebeec4fb36b52c442107499c18a896a2a/multidict-6.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912", size = 43023, upload-time = "2025-10-06T14:49:19.648Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877, upload-time = "2025-10-06T14:49:20.884Z" }, + { url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467, upload-time = "2025-10-06T14:49:22.054Z" }, + { url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834, upload-time = "2025-10-06T14:49:23.566Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f5/013798161ca665e4a422afbc5e2d9e4070142a9ff8905e482139cd09e4d0/multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", size = 250545, upload-time = "2025-10-06T14:49:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/71/2f/91dbac13e0ba94669ea5119ba267c9a832f0cb65419aca75549fcf09a3dc/multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", size = 258305, upload-time = "2025-10-06T14:49:26.778Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b0/754038b26f6e04488b48ac621f779c341338d78503fb45403755af2df477/multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", size = 242363, upload-time = "2025-10-06T14:49:28.562Z" }, + { url = "https://files.pythonhosted.org/packages/87/15/9da40b9336a7c9fa606c4cf2ed80a649dffeb42b905d4f63a1d7eb17d746/multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", size = 268375, upload-time = "2025-10-06T14:49:29.96Z" }, + { url = "https://files.pythonhosted.org/packages/82/72/c53fcade0cc94dfaad583105fd92b3a783af2091eddcb41a6d5a52474000/multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", size = 269346, upload-time = "2025-10-06T14:49:31.404Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e2/9baffdae21a76f77ef8447f1a05a96ec4bc0a24dae08767abc0a2fe680b8/multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", size = 256107, upload-time = "2025-10-06T14:49:32.974Z" }, + { url = "https://files.pythonhosted.org/packages/3c/06/3f06f611087dc60d65ef775f1fb5aca7c6d61c6db4990e7cda0cef9b1651/multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", size = 253592, upload-time = "2025-10-06T14:49:34.52Z" }, + { url = "https://files.pythonhosted.org/packages/20/24/54e804ec7945b6023b340c412ce9c3f81e91b3bf5fa5ce65558740141bee/multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", size = 251024, upload-time = "2025-10-06T14:49:35.956Z" }, + { url = "https://files.pythonhosted.org/packages/14/48/011cba467ea0b17ceb938315d219391d3e421dfd35928e5dbdc3f4ae76ef/multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", size = 251484, upload-time = "2025-10-06T14:49:37.631Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579, upload-time = "2025-10-06T14:49:39.502Z" }, + { url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654, upload-time = "2025-10-06T14:49:41.32Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511, upload-time = "2025-10-06T14:49:46.021Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895, upload-time = "2025-10-06T14:49:48.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073, upload-time = "2025-10-06T14:49:50.28Z" }, + { url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226, upload-time = "2025-10-06T14:49:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135, upload-time = "2025-10-06T14:49:54.26Z" }, + { url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117, upload-time = "2025-10-06T14:49:55.82Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472, upload-time = "2025-10-06T14:49:57.048Z" }, + { url = "https://files.pythonhosted.org/packages/75/3f/e2639e80325af0b6c6febdf8e57cc07043ff15f57fa1ef808f4ccb5ac4cd/multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", size = 249342, upload-time = "2025-10-06T14:49:58.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/cc/84e0585f805cbeaa9cbdaa95f9a3d6aed745b9d25700623ac89a6ecff400/multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", size = 257082, upload-time = "2025-10-06T14:49:59.89Z" }, + { url = "https://files.pythonhosted.org/packages/b0/9c/ac851c107c92289acbbf5cfb485694084690c1b17e555f44952c26ddc5bd/multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", size = 240704, upload-time = "2025-10-06T14:50:01.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/cc/5f93e99427248c09da95b62d64b25748a5f5c98c7c2ab09825a1d6af0e15/multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", size = 266355, upload-time = "2025-10-06T14:50:02.955Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0c/2ec1d883ceb79c6f7f6d7ad90c919c898f5d1c6ea96d322751420211e072/multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", size = 267259, upload-time = "2025-10-06T14:50:04.446Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", size = 254903, upload-time = "2025-10-06T14:50:05.98Z" }, + { url = "https://files.pythonhosted.org/packages/06/c9/11ea263ad0df7dfabcad404feb3c0dd40b131bc7f232d5537f2fb1356951/multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", size = 252365, upload-time = "2025-10-06T14:50:07.511Z" }, + { url = "https://files.pythonhosted.org/packages/41/88/d714b86ee2c17d6e09850c70c9d310abac3d808ab49dfa16b43aba9d53fd/multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", size = 250062, upload-time = "2025-10-06T14:50:09.074Z" }, + { url = "https://files.pythonhosted.org/packages/15/fe/ad407bb9e818c2b31383f6131ca19ea7e35ce93cf1310fce69f12e89de75/multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", size = 249683, upload-time = "2025-10-06T14:50:10.714Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/a89abdb0229e533fb925e7c6e5c40201c2873efebc9abaf14046a4536ee6/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", size = 261254, upload-time = "2025-10-06T14:50:12.28Z" }, + { url = "https://files.pythonhosted.org/packages/8d/aa/0e2b27bd88b40a4fb8dc53dd74eecac70edaa4c1dd0707eb2164da3675b3/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", size = 257967, upload-time = "2025-10-06T14:50:14.16Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8e/0c67b7120d5d5f6d874ed85a085f9dc770a7f9d8813e80f44a9fec820bb7/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", size = 250085, upload-time = "2025-10-06T14:50:15.639Z" }, + { url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713, upload-time = "2025-10-06T14:50:17.066Z" }, + { url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915, upload-time = "2025-10-06T14:50:18.264Z" }, + { url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077, upload-time = "2025-10-06T14:50:19.853Z" }, + { url = "https://files.pythonhosted.org/packages/e8/68/7b3a5170a382a340147337b300b9eb25a9ddb573bcdfff19c0fa3f31ffba/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", size = 83114, upload-time = "2025-10-06T14:50:21.223Z" }, + { url = "https://files.pythonhosted.org/packages/55/5c/3fa2d07c84df4e302060f555bbf539310980362236ad49f50eeb0a1c1eb9/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", size = 48442, upload-time = "2025-10-06T14:50:22.871Z" }, + { url = "https://files.pythonhosted.org/packages/fc/56/67212d33239797f9bd91962bb899d72bb0f4c35a8652dcdb8ed049bef878/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", size = 46885, upload-time = "2025-10-06T14:50:24.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/d1/908f896224290350721597a61a69cd19b89ad8ee0ae1f38b3f5cd12ea2ac/multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", size = 242588, upload-time = "2025-10-06T14:50:25.716Z" }, + { url = "https://files.pythonhosted.org/packages/ab/67/8604288bbd68680eee0ab568fdcb56171d8b23a01bcd5cb0c8fedf6e5d99/multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", size = 249966, upload-time = "2025-10-06T14:50:28.192Z" }, + { url = "https://files.pythonhosted.org/packages/20/33/9228d76339f1ba51e3efef7da3ebd91964d3006217aae13211653193c3ff/multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", size = 228618, upload-time = "2025-10-06T14:50:29.82Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2d/25d9b566d10cab1c42b3b9e5b11ef79c9111eaf4463b8c257a3bd89e0ead/multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", size = 257539, upload-time = "2025-10-06T14:50:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b1/8d1a965e6637fc33de3c0d8f414485c2b7e4af00f42cab3d84e7b955c222/multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", size = 256345, upload-time = "2025-10-06T14:50:33.26Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0c/06b5a8adbdeedada6f4fb8d8f193d44a347223b11939b42953eeb6530b6b/multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", size = 247934, upload-time = "2025-10-06T14:50:34.808Z" }, + { url = "https://files.pythonhosted.org/packages/8f/31/b2491b5fe167ca044c6eb4b8f2c9f3b8a00b24c432c365358eadac5d7625/multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", size = 245243, upload-time = "2025-10-06T14:50:36.436Z" }, + { url = "https://files.pythonhosted.org/packages/61/1a/982913957cb90406c8c94f53001abd9eafc271cb3e70ff6371590bec478e/multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", size = 235878, upload-time = "2025-10-06T14:50:37.953Z" }, + { url = "https://files.pythonhosted.org/packages/be/c0/21435d804c1a1cf7a2608593f4d19bca5bcbd7a81a70b253fdd1c12af9c0/multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", size = 243452, upload-time = "2025-10-06T14:50:39.574Z" }, + { url = "https://files.pythonhosted.org/packages/54/0a/4349d540d4a883863191be6eb9a928846d4ec0ea007d3dcd36323bb058ac/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", size = 252312, upload-time = "2025-10-06T14:50:41.612Z" }, + { url = "https://files.pythonhosted.org/packages/26/64/d5416038dbda1488daf16b676e4dbfd9674dde10a0cc8f4fc2b502d8125d/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", size = 246935, upload-time = "2025-10-06T14:50:43.972Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8c/8290c50d14e49f35e0bd4abc25e1bc7711149ca9588ab7d04f886cdf03d9/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", size = 243385, upload-time = "2025-10-06T14:50:45.648Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777, upload-time = "2025-10-06T14:50:47.154Z" }, + { url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104, upload-time = "2025-10-06T14:50:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503, upload-time = "2025-10-06T14:50:50.16Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b1/3da6934455dd4b261d4c72f897e3a5728eba81db59959f3a639245891baa/multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", size = 75128, upload-time = "2025-10-06T14:50:51.92Z" }, + { url = "https://files.pythonhosted.org/packages/14/2c/f069cab5b51d175a1a2cb4ccdf7a2c2dabd58aa5bd933fa036a8d15e2404/multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", size = 44410, upload-time = "2025-10-06T14:50:53.275Z" }, + { url = "https://files.pythonhosted.org/packages/42/e2/64bb41266427af6642b6b128e8774ed84c11b80a90702c13ac0a86bb10cc/multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", size = 43205, upload-time = "2025-10-06T14:50:54.911Z" }, + { url = "https://files.pythonhosted.org/packages/02/68/6b086fef8a3f1a8541b9236c594f0c9245617c29841f2e0395d979485cde/multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128", size = 245084, upload-time = "2025-10-06T14:50:56.369Z" }, + { url = "https://files.pythonhosted.org/packages/15/ee/f524093232007cd7a75c1d132df70f235cfd590a7c9eaccd7ff422ef4ae8/multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34", size = 252667, upload-time = "2025-10-06T14:50:57.991Z" }, + { url = "https://files.pythonhosted.org/packages/02/a5/eeb3f43ab45878f1895118c3ef157a480db58ede3f248e29b5354139c2c9/multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99", size = 233590, upload-time = "2025-10-06T14:50:59.589Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/76d02f8270b97269d7e3dbd45644b1785bda457b474315f8cf999525a193/multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202", size = 264112, upload-time = "2025-10-06T14:51:01.183Z" }, + { url = "https://files.pythonhosted.org/packages/76/0b/c28a70ecb58963847c2a8efe334904cd254812b10e535aefb3bcce513918/multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1", size = 261194, upload-time = "2025-10-06T14:51:02.794Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/2ab26e4209773223159b83aa32721b4021ffb08102f8ac7d689c943fded1/multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3", size = 248510, upload-time = "2025-10-06T14:51:04.724Z" }, + { url = "https://files.pythonhosted.org/packages/93/cd/06c1fa8282af1d1c46fd55c10a7930af652afdce43999501d4d68664170c/multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d", size = 248395, upload-time = "2025-10-06T14:51:06.306Z" }, + { url = "https://files.pythonhosted.org/packages/99/ac/82cb419dd6b04ccf9e7e61befc00c77614fc8134362488b553402ecd55ce/multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6", size = 239520, upload-time = "2025-10-06T14:51:08.091Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f3/a0f9bf09493421bd8716a362e0cd1d244f5a6550f5beffdd6b47e885b331/multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7", size = 245479, upload-time = "2025-10-06T14:51:10.365Z" }, + { url = "https://files.pythonhosted.org/packages/8d/01/476d38fc73a212843f43c852b0eee266b6971f0e28329c2184a8df90c376/multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", size = 258903, upload-time = "2025-10-06T14:51:12.466Z" }, + { url = "https://files.pythonhosted.org/packages/49/6d/23faeb0868adba613b817d0e69c5f15531b24d462af8012c4f6de4fa8dc3/multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", size = 252333, upload-time = "2025-10-06T14:51:14.48Z" }, + { url = "https://files.pythonhosted.org/packages/1e/cc/48d02ac22b30fa247f7dad82866e4b1015431092f4ba6ebc7e77596e0b18/multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", size = 243411, upload-time = "2025-10-06T14:51:16.072Z" }, + { url = "https://files.pythonhosted.org/packages/4a/03/29a8bf5a18abf1fe34535c88adbdfa88c9fb869b5a3b120692c64abe8284/multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", size = 40940, upload-time = "2025-10-06T14:51:17.544Z" }, + { url = "https://files.pythonhosted.org/packages/82/16/7ed27b680791b939de138f906d5cf2b4657b0d45ca6f5dd6236fdddafb1a/multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", size = 45087, upload-time = "2025-10-06T14:51:18.875Z" }, + { url = "https://files.pythonhosted.org/packages/cd/3c/e3e62eb35a1950292fe39315d3c89941e30a9d07d5d2df42965ab041da43/multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", size = 42368, upload-time = "2025-10-06T14:51:20.225Z" }, + { url = "https://files.pythonhosted.org/packages/8b/40/cd499bd0dbc5f1136726db3153042a735fffd0d77268e2ee20d5f33c010f/multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", size = 82326, upload-time = "2025-10-06T14:51:21.588Z" }, + { url = "https://files.pythonhosted.org/packages/13/8a/18e031eca251c8df76daf0288e6790561806e439f5ce99a170b4af30676b/multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", size = 48065, upload-time = "2025-10-06T14:51:22.93Z" }, + { url = "https://files.pythonhosted.org/packages/40/71/5e6701277470a87d234e433fb0a3a7deaf3bcd92566e421e7ae9776319de/multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", size = 46475, upload-time = "2025-10-06T14:51:24.352Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6a/bab00cbab6d9cfb57afe1663318f72ec28289ea03fd4e8236bb78429893a/multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e", size = 239324, upload-time = "2025-10-06T14:51:25.822Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5f/8de95f629fc22a7769ade8b41028e3e5a822c1f8904f618d175945a81ad3/multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", size = 246877, upload-time = "2025-10-06T14:51:27.604Z" }, + { url = "https://files.pythonhosted.org/packages/23/b4/38881a960458f25b89e9f4a4fdcb02ac101cfa710190db6e5528841e67de/multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", size = 225824, upload-time = "2025-10-06T14:51:29.664Z" }, + { url = "https://files.pythonhosted.org/packages/1e/39/6566210c83f8a261575f18e7144736059f0c460b362e96e9cf797a24b8e7/multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", size = 253558, upload-time = "2025-10-06T14:51:31.684Z" }, + { url = "https://files.pythonhosted.org/packages/00/a3/67f18315100f64c269f46e6c0319fa87ba68f0f64f2b8e7fd7c72b913a0b/multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", size = 252339, upload-time = "2025-10-06T14:51:33.699Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2a/1cb77266afee2458d82f50da41beba02159b1d6b1f7973afc9a1cad1499b/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", size = 244895, upload-time = "2025-10-06T14:51:36.189Z" }, + { url = "https://files.pythonhosted.org/packages/dd/72/09fa7dd487f119b2eb9524946ddd36e2067c08510576d43ff68469563b3b/multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", size = 241862, upload-time = "2025-10-06T14:51:41.291Z" }, + { url = "https://files.pythonhosted.org/packages/65/92/bc1f8bd0853d8669300f732c801974dfc3702c3eeadae2f60cef54dc69d7/multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", size = 232376, upload-time = "2025-10-06T14:51:43.55Z" }, + { url = "https://files.pythonhosted.org/packages/09/86/ac39399e5cb9d0c2ac8ef6e10a768e4d3bc933ac808d49c41f9dc23337eb/multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394", size = 240272, upload-time = "2025-10-06T14:51:45.265Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b6/fed5ac6b8563ec72df6cb1ea8dac6d17f0a4a1f65045f66b6d3bf1497c02/multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", size = 248774, upload-time = "2025-10-06T14:51:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8d/b954d8c0dc132b68f760aefd45870978deec6818897389dace00fcde32ff/multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", size = 242731, upload-time = "2025-10-06T14:51:48.541Z" }, + { url = "https://files.pythonhosted.org/packages/16/9d/a2dac7009125d3540c2f54e194829ea18ac53716c61b655d8ed300120b0f/multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", size = 240193, upload-time = "2025-10-06T14:51:50.355Z" }, + { url = "https://files.pythonhosted.org/packages/39/ca/c05f144128ea232ae2178b008d5011d4e2cea86e4ee8c85c2631b1b94802/multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", size = 48023, upload-time = "2025-10-06T14:51:51.883Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8f/0a60e501584145588be1af5cc829265701ba3c35a64aec8e07cbb71d39bb/multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", size = 53507, upload-time = "2025-10-06T14:51:53.672Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ae/3148b988a9c6239903e786eac19c889fab607c31d6efa7fb2147e5680f23/multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", size = 44804, upload-time = "2025-10-06T14:51:55.415Z" }, + { url = "https://files.pythonhosted.org/packages/90/d7/4cf84257902265c4250769ac49f4eaab81c182ee9aff8bf59d2714dbb174/multidict-6.7.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:363eb68a0a59bd2303216d2346e6c441ba10d36d1f9969fcb6f1ba700de7bb5c", size = 77073, upload-time = "2025-10-06T14:51:57.386Z" }, + { url = "https://files.pythonhosted.org/packages/6d/51/194e999630a656e76c2965a1590d12faa5cd528170f2abaa04423e09fe8d/multidict-6.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d874eb056410ca05fed180b6642e680373688efafc7f077b2a2f61811e873a40", size = 44928, upload-time = "2025-10-06T14:51:58.791Z" }, + { url = "https://files.pythonhosted.org/packages/e5/6b/2a195373c33068c9158e0941d0b46cfcc9c1d894ca2eb137d1128081dff0/multidict-6.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b55d5497b51afdfde55925e04a022f1de14d4f4f25cdfd4f5d9b0aa96166851", size = 44581, upload-time = "2025-10-06T14:52:00.174Z" }, + { url = "https://files.pythonhosted.org/packages/69/7b/7f4f2e644b6978bf011a5fd9a5ebb7c21de3f38523b1f7897d36a1ac1311/multidict-6.7.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f8e5c0031b90ca9ce555e2e8fd5c3b02a25f14989cbc310701823832c99eb687", size = 239901, upload-time = "2025-10-06T14:52:02.416Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b5/952c72786710a031aa204a9adf7db66d7f97a2c6573889d58b9e60fe6702/multidict-6.7.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cf41880c991716f3c7cec48e2f19ae4045fc9db5fc9cff27347ada24d710bb5", size = 240534, upload-time = "2025-10-06T14:52:04.105Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ef/109fe1f2471e4c458c74242c7e4a833f2d9fc8a6813cd7ee345b0bad18f9/multidict-6.7.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cfc12a8630a29d601f48d47787bd7eb730e475e83edb5d6c5084317463373eb", size = 219545, upload-time = "2025-10-06T14:52:06.208Z" }, + { url = "https://files.pythonhosted.org/packages/42/bd/327d91288114967f9fe90dc53de70aa3fec1b9073e46aa32c4828f771a87/multidict-6.7.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3996b50c3237c4aec17459217c1e7bbdead9a22a0fcd3c365564fbd16439dde6", size = 251187, upload-time = "2025-10-06T14:52:08.049Z" }, + { url = "https://files.pythonhosted.org/packages/f4/13/a8b078ebbaceb7819fd28cd004413c33b98f1b70d542a62e6a00b74fb09f/multidict-6.7.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7f5170993a0dd3ab871c74f45c0a21a4e2c37a2f2b01b5f722a2ad9c6650469e", size = 249379, upload-time = "2025-10-06T14:52:09.831Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6d/ab12e1246be4d65d1f55de1e6f6aaa9b8120eddcfdd1d290439c7833d5ce/multidict-6.7.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ec81878ddf0e98817def1e77d4f50dae5ef5b0e4fe796fae3bd674304172416e", size = 239241, upload-time = "2025-10-06T14:52:11.561Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d7/079a93625208c173b8fa756396814397c0fd9fee61ef87b75a748820b86e/multidict-6.7.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9281bf5b34f59afbc6b1e477a372e9526b66ca446f4bf62592839c195a718b32", size = 237418, upload-time = "2025-10-06T14:52:13.671Z" }, + { url = "https://files.pythonhosted.org/packages/c9/29/03777c2212274aa9440918d604dc9d6af0e6b4558c611c32c3dcf1a13870/multidict-6.7.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:68af405971779d8b37198726f2b6fe3955db846fee42db7a4286fc542203934c", size = 232987, upload-time = "2025-10-06T14:52:15.708Z" }, + { url = "https://files.pythonhosted.org/packages/d9/00/11188b68d85a84e8050ee34724d6ded19ad03975caebe0c8dcb2829b37bf/multidict-6.7.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3ba3ef510467abb0667421a286dc906e30eb08569365f5cdb131d7aff7c2dd84", size = 240985, upload-time = "2025-10-06T14:52:17.317Z" }, + { url = "https://files.pythonhosted.org/packages/df/0c/12eef6aeda21859c6cdf7d75bd5516d83be3efe3d8cc45fd1a3037f5b9dc/multidict-6.7.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b61189b29081a20c7e4e0b49b44d5d44bb0dc92be3c6d06a11cc043f81bf9329", size = 246855, upload-time = "2025-10-06T14:52:19.096Z" }, + { url = "https://files.pythonhosted.org/packages/69/f6/076120fd8bb3975f09228e288e08bff6b9f1bfd5166397c7ba284f622ab2/multidict-6.7.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fb287618b9c7aa3bf8d825f02d9201b2f13078a5ed3b293c8f4d953917d84d5e", size = 241804, upload-time = "2025-10-06T14:52:21.166Z" }, + { url = "https://files.pythonhosted.org/packages/5f/51/41bb950c81437b88a93e6ddfca1d8763569ae861e638442838c4375f7497/multidict-6.7.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:521f33e377ff64b96c4c556b81c55d0cfffb96a11c194fd0c3f1e56f3d8dd5a4", size = 235321, upload-time = "2025-10-06T14:52:23.208Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cf/5bbd31f055199d56c1f6b04bbadad3ccb24e6d5d4db75db774fc6d6674b8/multidict-6.7.0-cp39-cp39-win32.whl", hash = "sha256:ce8fdc2dca699f8dbf055a61d73eaa10482569ad20ee3c36ef9641f69afa8c91", size = 41435, upload-time = "2025-10-06T14:52:24.735Z" }, + { url = "https://files.pythonhosted.org/packages/af/01/547ffe9c2faec91c26965c152f3fea6cff068b6037401f61d310cc861ff4/multidict-6.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:7e73299c99939f089dd9b2120a04a516b95cdf8c1cd2b18c53ebf0de80b1f18f", size = 46193, upload-time = "2025-10-06T14:52:26.101Z" }, + { url = "https://files.pythonhosted.org/packages/27/77/cfa5461d1d2651d6fc24216c92b4a21d4e385a41c46e0d9f3b070675167b/multidict-6.7.0-cp39-cp39-win_arm64.whl", hash = "sha256:6bdce131e14b04fd34a809b6380dbfd826065c3e2fe8a50dbae659fa0c390546", size = 43118, upload-time = "2025-10-06T14:52:27.876Z" }, + { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, +] + +[[package]] +name = "mypy" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/e3/034322d5a779685218ed69286c32faa505247f1f096251ef66c8fd203b08/mypy-1.17.0.tar.gz", hash = "sha256:e5d7ccc08ba089c06e2f5629c660388ef1fee708444f1dee0b9203fa031dee03", size = 3352114, upload-time = "2025-07-14T20:34:30.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/31/e762baa3b73905c856d45ab77b4af850e8159dffffd86a52879539a08c6b/mypy-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8e08de6138043108b3b18f09d3f817a4783912e48828ab397ecf183135d84d6", size = 10998313, upload-time = "2025-07-14T20:33:24.519Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c1/25b2f0d46fb7e0b5e2bee61ec3a47fe13eff9e3c2f2234f144858bbe6485/mypy-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce4a17920ec144647d448fc43725b5873548b1aae6c603225626747ededf582d", size = 10128922, upload-time = "2025-07-14T20:34:06.414Z" }, + { url = "https://files.pythonhosted.org/packages/02/78/6d646603a57aa8a2886df1b8881fe777ea60f28098790c1089230cd9c61d/mypy-1.17.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ff25d151cc057fdddb1cb1881ef36e9c41fa2a5e78d8dd71bee6e4dcd2bc05b", size = 11913524, upload-time = "2025-07-14T20:33:19.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/19/dae6c55e87ee426fb76980f7e78484450cad1c01c55a1dc4e91c930bea01/mypy-1.17.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93468cf29aa9a132bceb103bd8475f78cacde2b1b9a94fd978d50d4bdf616c9a", size = 12650527, upload-time = "2025-07-14T20:32:44.095Z" }, + { url = "https://files.pythonhosted.org/packages/86/e1/f916845a235235a6c1e4d4d065a3930113767001d491b8b2e1b61ca56647/mypy-1.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:98189382b310f16343151f65dd7e6867386d3e35f7878c45cfa11383d175d91f", size = 12897284, upload-time = "2025-07-14T20:33:38.168Z" }, + { url = "https://files.pythonhosted.org/packages/ae/dc/414760708a4ea1b096bd214d26a24e30ac5e917ef293bc33cdb6fe22d2da/mypy-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:c004135a300ab06a045c1c0d8e3f10215e71d7b4f5bb9a42ab80236364429937", size = 9506493, upload-time = "2025-07-14T20:34:01.093Z" }, + { url = "https://files.pythonhosted.org/packages/d4/24/82efb502b0b0f661c49aa21cfe3e1999ddf64bf5500fc03b5a1536a39d39/mypy-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d4fe5c72fd262d9c2c91c1117d16aac555e05f5beb2bae6a755274c6eec42be", size = 10914150, upload-time = "2025-07-14T20:31:51.985Z" }, + { url = "https://files.pythonhosted.org/packages/03/96/8ef9a6ff8cedadff4400e2254689ca1dc4b420b92c55255b44573de10c54/mypy-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96b196e5c16f41b4f7736840e8455958e832871990c7ba26bf58175e357ed61", size = 10039845, upload-time = "2025-07-14T20:32:30.527Z" }, + { url = "https://files.pythonhosted.org/packages/df/32/7ce359a56be779d38021d07941cfbb099b41411d72d827230a36203dbb81/mypy-1.17.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73a0ff2dd10337ceb521c080d4147755ee302dcde6e1a913babd59473904615f", size = 11837246, upload-time = "2025-07-14T20:32:01.28Z" }, + { url = "https://files.pythonhosted.org/packages/82/16/b775047054de4d8dbd668df9137707e54b07fe18c7923839cd1e524bf756/mypy-1.17.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24cfcc1179c4447854e9e406d3af0f77736d631ec87d31c6281ecd5025df625d", size = 12571106, upload-time = "2025-07-14T20:34:26.942Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/fa33eaf29a606102c8d9ffa45a386a04c2203d9ad18bf4eef3e20c43ebc8/mypy-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c56f180ff6430e6373db7a1d569317675b0a451caf5fef6ce4ab365f5f2f6c3", size = 12759960, upload-time = "2025-07-14T20:33:42.882Z" }, + { url = "https://files.pythonhosted.org/packages/94/75/3f5a29209f27e739ca57e6350bc6b783a38c7621bdf9cac3ab8a08665801/mypy-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:eafaf8b9252734400f9b77df98b4eee3d2eecab16104680d51341c75702cad70", size = 9503888, upload-time = "2025-07-14T20:32:34.392Z" }, + { url = "https://files.pythonhosted.org/packages/12/e9/e6824ed620bbf51d3bf4d6cbbe4953e83eaf31a448d1b3cfb3620ccb641c/mypy-1.17.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f986f1cab8dbec39ba6e0eaa42d4d3ac6686516a5d3dccd64be095db05ebc6bb", size = 11086395, upload-time = "2025-07-14T20:34:11.452Z" }, + { url = "https://files.pythonhosted.org/packages/ba/51/a4afd1ae279707953be175d303f04a5a7bd7e28dc62463ad29c1c857927e/mypy-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:51e455a54d199dd6e931cd7ea987d061c2afbaf0960f7f66deef47c90d1b304d", size = 10120052, upload-time = "2025-07-14T20:33:09.897Z" }, + { url = "https://files.pythonhosted.org/packages/8a/71/19adfeac926ba8205f1d1466d0d360d07b46486bf64360c54cb5a2bd86a8/mypy-1.17.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3204d773bab5ff4ebbd1f8efa11b498027cd57017c003ae970f310e5b96be8d8", size = 11861806, upload-time = "2025-07-14T20:32:16.028Z" }, + { url = "https://files.pythonhosted.org/packages/0b/64/d6120eca3835baf7179e6797a0b61d6c47e0bc2324b1f6819d8428d5b9ba/mypy-1.17.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1051df7ec0886fa246a530ae917c473491e9a0ba6938cfd0ec2abc1076495c3e", size = 12744371, upload-time = "2025-07-14T20:33:33.503Z" }, + { url = "https://files.pythonhosted.org/packages/1f/dc/56f53b5255a166f5bd0f137eed960e5065f2744509dfe69474ff0ba772a5/mypy-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f773c6d14dcc108a5b141b4456b0871df638eb411a89cd1c0c001fc4a9d08fc8", size = 12914558, upload-time = "2025-07-14T20:33:56.961Z" }, + { url = "https://files.pythonhosted.org/packages/69/ac/070bad311171badc9add2910e7f89271695a25c136de24bbafc7eded56d5/mypy-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:1619a485fd0e9c959b943c7b519ed26b712de3002d7de43154a489a2d0fd817d", size = 9585447, upload-time = "2025-07-14T20:32:20.594Z" }, + { url = "https://files.pythonhosted.org/packages/be/7b/5f8ab461369b9e62157072156935cec9d272196556bdc7c2ff5f4c7c0f9b/mypy-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c41aa59211e49d717d92b3bb1238c06d387c9325d3122085113c79118bebb06", size = 11070019, upload-time = "2025-07-14T20:32:07.99Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f8/c49c9e5a2ac0badcc54beb24e774d2499748302c9568f7f09e8730e953fa/mypy-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e69db1fb65b3114f98c753e3930a00514f5b68794ba80590eb02090d54a5d4a", size = 10114457, upload-time = "2025-07-14T20:33:47.285Z" }, + { url = "https://files.pythonhosted.org/packages/89/0c/fb3f9c939ad9beed3e328008b3fb90b20fda2cddc0f7e4c20dbefefc3b33/mypy-1.17.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03ba330b76710f83d6ac500053f7727270b6b8553b0423348ffb3af6f2f7b889", size = 11857838, upload-time = "2025-07-14T20:33:14.462Z" }, + { url = "https://files.pythonhosted.org/packages/4c/66/85607ab5137d65e4f54d9797b77d5a038ef34f714929cf8ad30b03f628df/mypy-1.17.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:037bc0f0b124ce46bfde955c647f3e395c6174476a968c0f22c95a8d2f589bba", size = 12731358, upload-time = "2025-07-14T20:32:25.579Z" }, + { url = "https://files.pythonhosted.org/packages/73/d0/341dbbfb35ce53d01f8f2969facbb66486cee9804048bf6c01b048127501/mypy-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c38876106cb6132259683632b287238858bd58de267d80defb6f418e9ee50658", size = 12917480, upload-time = "2025-07-14T20:34:21.868Z" }, + { url = "https://files.pythonhosted.org/packages/64/63/70c8b7dbfc520089ac48d01367a97e8acd734f65bd07813081f508a8c94c/mypy-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:d30ba01c0f151998f367506fab31c2ac4527e6a7b2690107c7a7f9e3cb419a9c", size = 9589666, upload-time = "2025-07-14T20:34:16.841Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a0/6263dd11941231f688f0a8f2faf90ceac1dc243d148d314a089d2fe25108/mypy-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:63e751f1b5ab51d6f3d219fe3a2fe4523eaa387d854ad06906c63883fde5b1ab", size = 10988185, upload-time = "2025-07-14T20:33:04.797Z" }, + { url = "https://files.pythonhosted.org/packages/02/13/b8f16d6b0dc80277129559c8e7dbc9011241a0da8f60d031edb0e6e9ac8f/mypy-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f7fb09d05e0f1c329a36dcd30e27564a3555717cde87301fae4fb542402ddfad", size = 10120169, upload-time = "2025-07-14T20:32:38.84Z" }, + { url = "https://files.pythonhosted.org/packages/14/ef/978ba79df0d65af680e20d43121363cf643eb79b04bf3880d01fc8afeb6f/mypy-1.17.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b72c34ce05ac3a1361ae2ebb50757fb6e3624032d91488d93544e9f82db0ed6c", size = 11918121, upload-time = "2025-07-14T20:33:52.328Z" }, + { url = "https://files.pythonhosted.org/packages/f4/10/55ef70b104151a0d8280474f05268ff0a2a79be8d788d5e647257d121309/mypy-1.17.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:434ad499ad8dde8b2f6391ddfa982f41cb07ccda8e3c67781b1bfd4e5f9450a8", size = 12648821, upload-time = "2025-07-14T20:32:59.631Z" }, + { url = "https://files.pythonhosted.org/packages/26/8c/7781fcd2e1eef48fbedd3a422c21fe300a8e03ed5be2eb4bd10246a77f4e/mypy-1.17.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f105f61a5eff52e137fd73bee32958b2add9d9f0a856f17314018646af838e97", size = 12896955, upload-time = "2025-07-14T20:32:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/78/13/03ac759dabe86e98ca7b6681f114f90ee03f3ff8365a57049d311bd4a4e3/mypy-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:ba06254a5a22729853209550d80f94e28690d5530c661f9416a68ac097b13fc4", size = 9512957, upload-time = "2025-07-14T20:33:28.619Z" }, + { url = "https://files.pythonhosted.org/packages/e3/fc/ee058cc4316f219078464555873e99d170bde1d9569abd833300dbeb484a/mypy-1.17.0-py3-none-any.whl", hash = "sha256:15d9d0018237ab058e5de3d8fce61b6fa72cc59cc78fd91f1b474bce12abf496", size = 2283195, upload-time = "2025-07-14T20:31:54.753Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/0e/934b541323035566a9af292dba85a195f7b78179114f2c6ebb24551118a9/propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", size = 79534, upload-time = "2025-10-08T19:46:02.083Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/db0d03d96726d995dc7171286c6ba9d8d14251f37433890f88368951a44e/propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", size = 45526, upload-time = "2025-10-08T19:46:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c3/82728404aea669e1600f304f2609cde9e665c18df5a11cdd57ed73c1dceb/propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", size = 47263, upload-time = "2025-10-08T19:46:05.405Z" }, + { url = "https://files.pythonhosted.org/packages/df/1b/39313ddad2bf9187a1432654c38249bab4562ef535ef07f5eb6eb04d0b1b/propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", size = 201012, upload-time = "2025-10-08T19:46:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/5b/01/f1d0b57d136f294a142acf97f4ed58c8e5b974c21e543000968357115011/propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", size = 209491, upload-time = "2025-10-08T19:46:08.909Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c8/038d909c61c5bb039070b3fb02ad5cccdb1dde0d714792e251cdb17c9c05/propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", size = 215319, upload-time = "2025-10-08T19:46:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/08/57/8c87e93142b2c1fa2408e45695205a7ba05fb5db458c0bf5c06ba0e09ea6/propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", size = 196856, upload-time = "2025-10-08T19:46:12.003Z" }, + { url = "https://files.pythonhosted.org/packages/42/df/5615fec76aa561987a534759b3686008a288e73107faa49a8ae5795a9f7a/propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", size = 193241, upload-time = "2025-10-08T19:46:13.495Z" }, + { url = "https://files.pythonhosted.org/packages/d5/21/62949eb3a7a54afe8327011c90aca7e03547787a88fb8bd9726806482fea/propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", size = 190552, upload-time = "2025-10-08T19:46:14.938Z" }, + { url = "https://files.pythonhosted.org/packages/30/ee/ab4d727dd70806e5b4de96a798ae7ac6e4d42516f030ee60522474b6b332/propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", size = 200113, upload-time = "2025-10-08T19:46:16.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0b/38b46208e6711b016aa8966a3ac793eee0d05c7159d8342aa27fc0bc365e/propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", size = 200778, upload-time = "2025-10-08T19:46:18.023Z" }, + { url = "https://files.pythonhosted.org/packages/cf/81/5abec54355ed344476bee711e9f04815d4b00a311ab0535599204eecc257/propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", size = 193047, upload-time = "2025-10-08T19:46:19.449Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b6/1f237c04e32063cb034acd5f6ef34ef3a394f75502e72703545631ab1ef6/propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", size = 38093, upload-time = "2025-10-08T19:46:20.643Z" }, + { url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", size = 41638, upload-time = "2025-10-08T19:46:21.935Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", size = 38229, upload-time = "2025-10-08T19:46:23.368Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, + { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, + { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, + { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/9b/01/0ebaec9003f5d619a7475165961f8e3083cf8644d704b60395df3601632d/propcache-0.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff", size = 80277, upload-time = "2025-10-08T19:48:36.647Z" }, + { url = "https://files.pythonhosted.org/packages/34/58/04af97ac586b4ef6b9026c3fd36ee7798b737a832f5d3440a4280dcebd3a/propcache-0.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb", size = 45865, upload-time = "2025-10-08T19:48:37.859Z" }, + { url = "https://files.pythonhosted.org/packages/7c/19/b65d98ae21384518b291d9939e24a8aeac4fdb5101b732576f8f7540e834/propcache-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac", size = 47636, upload-time = "2025-10-08T19:48:39.038Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/317048c6d91c356c7154dca5af019e6effeb7ee15fa6a6db327cc19e12b4/propcache-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888", size = 201126, upload-time = "2025-10-08T19:48:40.774Z" }, + { url = "https://files.pythonhosted.org/packages/71/69/0b2a7a5a6ee83292b4b997dbd80549d8ce7d40b6397c1646c0d9495f5a85/propcache-0.4.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc", size = 209837, upload-time = "2025-10-08T19:48:42.167Z" }, + { url = "https://files.pythonhosted.org/packages/a5/92/c699ac495a6698df6e497fc2de27af4b6ace10d8e76528357ce153722e45/propcache-0.4.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a", size = 215578, upload-time = "2025-10-08T19:48:43.56Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ee/14de81c5eb02c0ee4f500b4e39c4e1bd0677c06e72379e6ab18923c773fc/propcache-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88", size = 197187, upload-time = "2025-10-08T19:48:45.309Z" }, + { url = "https://files.pythonhosted.org/packages/1d/94/48dce9aaa6d8dd5a0859bad75158ec522546d4ac23f8e2f05fac469477dd/propcache-0.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00", size = 193478, upload-time = "2025-10-08T19:48:47.743Z" }, + { url = "https://files.pythonhosted.org/packages/60/b5/0516b563e801e1ace212afde869a0596a0d7115eec0b12d296d75633fb29/propcache-0.4.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0", size = 190650, upload-time = "2025-10-08T19:48:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/24/89/e0f7d4a5978cd56f8cd67735f74052f257dc471ec901694e430f0d1572fe/propcache-0.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e", size = 200251, upload-time = "2025-10-08T19:48:51.4Z" }, + { url = "https://files.pythonhosted.org/packages/06/7d/a1fac863d473876ed4406c914f2e14aa82d2f10dd207c9e16fc383cc5a24/propcache-0.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781", size = 200919, upload-time = "2025-10-08T19:48:53.227Z" }, + { url = "https://files.pythonhosted.org/packages/c3/4e/f86a256ff24944cf5743e4e6c6994e3526f6acfcfb55e21694c2424f758c/propcache-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183", size = 193211, upload-time = "2025-10-08T19:48:55.027Z" }, + { url = "https://files.pythonhosted.org/packages/6e/3f/3fbad5f4356b068f1b047d300a6ff2c66614d7030f078cd50be3fec04228/propcache-0.4.1-cp39-cp39-win32.whl", hash = "sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19", size = 38314, upload-time = "2025-10-08T19:48:56.792Z" }, + { url = "https://files.pythonhosted.org/packages/a4/45/d78d136c3a3d215677abb886785aae744da2c3005bcb99e58640c56529b1/propcache-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f", size = 41912, upload-time = "2025-10-08T19:48:57.995Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2a/b0632941f25139f4e58450b307242951f7c2717a5704977c6d5323a800af/propcache-0.4.1-cp39-cp39-win_arm64.whl", hash = "sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938", size = 38450, upload-time = "2025-10-08T19:48:59.349Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "pydantic" +version = "1.10.26" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version < '3.10'", +] +dependencies = [ + { name = "typing-extensions", marker = "extra == 'group-18-runloop-api-client-pydantic-v1'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/da/fd89f987a376c807cd81ea0eff4589aade783bbb702637b4734ef2c743a2/pydantic-1.10.26.tar.gz", hash = "sha256:8c6aa39b494c5af092e690127c283d84f363ac36017106a9e66cb33a22ac412e", size = 357906, upload-time = "2025-12-18T15:47:46.557Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/08/2587a6d4314e7539eec84acd062cb7b037638edb57a0335d20e4c5b8878c/pydantic-1.10.26-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f7ae36fa0ecef8d39884120f212e16c06bb096a38f523421278e2f39c1784546", size = 2444588, upload-time = "2025-12-18T15:46:28.882Z" }, + { url = "https://files.pythonhosted.org/packages/47/e6/10df5f08c105bcbb4adbee7d1108ff4b347702b110fed058f6a03f1c6b73/pydantic-1.10.26-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d95a76cf503f0f72ed7812a91de948440b2bf564269975738a4751e4fadeb572", size = 2255972, upload-time = "2025-12-18T15:46:31.72Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/fdb961e7adc2c31f394feba6f560ef2c74c446f0285e2c2eb87d2b7206c7/pydantic-1.10.26-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a943ce8e00ad708ed06a1d9df5b4fd28f5635a003b82a4908ece6f24c0b18464", size = 2857175, upload-time = "2025-12-18T15:46:34Z" }, + { url = "https://files.pythonhosted.org/packages/8f/6c/f21e27dda475d4c562bd01b5874284dd3180f336c1e669413b743ca8b278/pydantic-1.10.26-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:465ad8edb29b15c10b779b16431fe8e77c380098badf6db367b7a1d3e572cf53", size = 2947001, upload-time = "2025-12-18T15:46:35.922Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f6/27ea206232cbb6ec24dc4e4e8888a9a734f96a1eaf13504be4b30ef26aa7/pydantic-1.10.26-cp310-cp310-win_amd64.whl", hash = "sha256:80e6be6272839c8a7641d26ad569ab77772809dd78f91d0068dc0fc97f071945", size = 2066217, upload-time = "2025-12-18T15:46:37.614Z" }, + { url = "https://files.pythonhosted.org/packages/1d/c1/d521e64c8130e1ad9d22c270bed3fabcc0940c9539b076b639c88fd32a8d/pydantic-1.10.26-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:116233e53889bcc536f617e38c1b8337d7fa9c280f0fd7a4045947515a785637", size = 2428347, upload-time = "2025-12-18T15:46:39.41Z" }, + { url = "https://files.pythonhosted.org/packages/2c/08/f4b804a00c16e3ea994cb640a7c25c579b4f1fa674cde6a19fa0dfb0ae4f/pydantic-1.10.26-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c3cfdd361addb6eb64ccd26ac356ad6514cee06a61ab26b27e16b5ed53108f77", size = 2212605, upload-time = "2025-12-18T15:46:41.006Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/0df4b9efef29bbc5e39f247fcba99060d15946b4463d82a5589cf7923d71/pydantic-1.10.26-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e4451951a9a93bf9a90576f3e25240b47ee49ab5236adccb8eff6ac943adf0f", size = 2753560, upload-time = "2025-12-18T15:46:43.215Z" }, + { url = "https://files.pythonhosted.org/packages/68/66/6ab6c1d3a116d05d2508fce64f96e35242938fac07544d611e11d0d363a0/pydantic-1.10.26-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9858ed44c6bea5f29ffe95308db9e62060791c877766c67dd5f55d072c8612b5", size = 2859235, upload-time = "2025-12-18T15:46:45.112Z" }, + { url = "https://files.pythonhosted.org/packages/61/4e/f1676bb0fcdf6ed2ce4670d7d1fc1d6c3a06d84497644acfbe02649503f1/pydantic-1.10.26-cp311-cp311-win_amd64.whl", hash = "sha256:ac1089f723e2106ebde434377d31239e00870a7563245072968e5af5cc4d33df", size = 2066646, upload-time = "2025-12-18T15:46:46.816Z" }, + { url = "https://files.pythonhosted.org/packages/02/6c/cd97a5a776c4515e6ee2ae81c2f2c5be51376dda6c31f965d7746ce0019f/pydantic-1.10.26-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:468d5b9cacfcaadc76ed0a4645354ab6f263ec01a63fb6d05630ea1df6ae453f", size = 2433795, upload-time = "2025-12-18T15:46:49.321Z" }, + { url = "https://files.pythonhosted.org/packages/47/12/de20affa30dcef728fcf9cc98e13ff4438c7a630de8d2f90eb38eba0891c/pydantic-1.10.26-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2c1b0b914be31671000ca25cf7ea17fcaaa68cfeadf6924529c5c5aa24b7ab1f", size = 2227387, upload-time = "2025-12-18T15:46:50.877Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1d/9d65dcc5b8c17ba590f1f9f486e9306346831902318b7ee93f63516f4003/pydantic-1.10.26-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15b13b9f8ba8867095769e1156e0d7fbafa1f65b898dd40fd1c02e34430973cb", size = 2629594, upload-time = "2025-12-18T15:46:53.42Z" }, + { url = "https://files.pythonhosted.org/packages/3f/76/acb41409356789e23e1a7ef58f93821410c96409183ce314ddb58d97f23e/pydantic-1.10.26-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad7025ca324ae263d4313998e25078dcaec5f9ed0392c06dedb57e053cc8086b", size = 2745305, upload-time = "2025-12-18T15:46:55.987Z" }, + { url = "https://files.pythonhosted.org/packages/22/72/a98c0c5e527a66057d969fedd61675223c7975ade61acebbca9f1abd6dc0/pydantic-1.10.26-cp312-cp312-win_amd64.whl", hash = "sha256:4482b299874dabb88a6c3759e3d85c6557c407c3b586891f7d808d8a38b66b9c", size = 1937647, upload-time = "2025-12-18T15:46:57.905Z" }, + { url = "https://files.pythonhosted.org/packages/28/b9/17a5a5a421c23ac27486b977724a42c9d5f8b7f0f4aab054251066223900/pydantic-1.10.26-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1ae7913bb40a96c87e3d3f6fe4e918ef53bf181583de4e71824360a9b11aef1c", size = 2494599, upload-time = "2025-12-18T15:47:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8e/6e3bd4241076cf227b443d7577245dd5d181ecf40b3182fcb908bc8c197d/pydantic-1.10.26-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8154c13f58d4de5d3a856bb6c909c7370f41fb876a5952a503af6b975265f4ba", size = 2254391, upload-time = "2025-12-18T15:47:02.268Z" }, + { url = "https://files.pythonhosted.org/packages/a8/30/a1c4092eda2145ecbead6c92db489b223e101e1ba0da82576d0cf73dd422/pydantic-1.10.26-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f8af0507bf6118b054a9765fb2e402f18a8b70c964f420d95b525eb711122d62", size = 2609445, upload-time = "2025-12-18T15:47:04.909Z" }, + { url = "https://files.pythonhosted.org/packages/3a/2a/0491f1729ee4b7b6bc859ec22f69752f0c09bee1b66ac6f5f701136f34c3/pydantic-1.10.26-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dcb5a7318fb43189fde6af6f21ac7149c4bcbcfffc54bc87b5becddc46084847", size = 2732124, upload-time = "2025-12-18T15:47:07.464Z" }, + { url = "https://files.pythonhosted.org/packages/2a/56/b59f3b2f84e1df2b04ae768a1bb04d9f0288ff71b67cdcbb07683757b2c0/pydantic-1.10.26-cp313-cp313-win_amd64.whl", hash = "sha256:71cde228bc0600cf8619f0ee62db050d1880dcc477eba0e90b23011b4ee0f314", size = 1939888, upload-time = "2025-12-18T15:47:09.618Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8b/0c3dc02d4b97790b0f199bf933f677c14e7be4a8d21307c5f2daae06aa41/pydantic-1.10.26-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6b40730cc81d53d515dc0b8bb5c9b43fadb9bed46de4a3c03bd95e8571616dba", size = 2502689, upload-time = "2025-12-18T15:47:12.308Z" }, + { url = "https://files.pythonhosted.org/packages/d4/9d/d31aeea45542b2ae4b09ecba92b88aaba696b801c31919811aa979a1242d/pydantic-1.10.26-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c3bbb9c0eecdf599e4db9b372fa9cc55be12e80a0d9c6d307950a39050cb0e37", size = 2269494, upload-time = "2025-12-18T15:47:14.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/c1/3a4d069593283ca4dd0006039ba33644e21e432cddc09da706ac50441610/pydantic-1.10.26-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc2e3fe7bc4993626ef6b6fa855defafa1d6f8996aa1caef2deb83c5ac4d043a", size = 2620047, upload-time = "2025-12-18T15:47:17.089Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0e/340c3d29197d99c15ab04093d43bb9c9d0fd17c2a34b80cb9d36ed732b09/pydantic-1.10.26-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:36d9e46b588aaeb1dcd2409fa4c467fe0b331f3cc9f227b03a7a00643704e962", size = 2747625, upload-time = "2025-12-18T15:47:19.21Z" }, + { url = "https://files.pythonhosted.org/packages/1e/58/f12ab3727339b172c830b32151919456b67787cdfe8808b2568b322fb15c/pydantic-1.10.26-cp314-cp314-win_amd64.whl", hash = "sha256:81ce3c8616d12a7be31b4aadfd3434f78f6b44b75adbfaec2fe1ad4f7f999b8c", size = 1976436, upload-time = "2025-12-18T15:47:21.384Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8a/3a5a6267d5f03617b5c0f1985aa9fdfbafd33a50ef6dadd866a15ed4d123/pydantic-1.10.26-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:502b9d30d18a2dfaf81b7302f6ba0e5853474b1c96212449eb4db912cb604b7d", size = 2457039, upload-time = "2025-12-18T15:47:34.584Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/343ac0db26918a033ac6256c036d72c3b6eb1196b7de622e2e8a94b19079/pydantic-1.10.26-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0d8f6087bf697dec3bf7ffcd7fe8362674f16519f3151789f33cbe8f1d19fc15", size = 2266441, upload-time = "2025-12-18T15:47:36.807Z" }, + { url = "https://files.pythonhosted.org/packages/fc/36/1ab48136578608dba2f2a62e452f3db2083b474d4e49be5749c6ae0c123c/pydantic-1.10.26-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dd40a99c358419910c85e6f5d22f9c56684c25b5e7abc40879b3b4a52f34ae90", size = 2869383, upload-time = "2025-12-18T15:47:38.883Z" }, + { url = "https://files.pythonhosted.org/packages/a2/25/41dbf1bffc31eb242cece8080561a4133eaeb513372dec36a84477a3fb71/pydantic-1.10.26-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ce3293b86ca9f4125df02ff0a70be91bc7946522467cbd98e7f1493f340616ba", size = 2963582, upload-time = "2025-12-18T15:47:40.854Z" }, + { url = "https://files.pythonhosted.org/packages/61/2f/f072ae160a300c85eb9f059915101fd33dacf12d8df08c2b804acb3b95d1/pydantic-1.10.26-cp39-cp39-win_amd64.whl", hash = "sha256:1a4e3062b71ab1d5df339ba12c48f9ed5817c5de6cb92a961dd5c64bb32e7b96", size = 2075530, upload-time = "2025-12-18T15:47:43.181Z" }, + { url = "https://files.pythonhosted.org/packages/1f/98/556e82f00b98486def0b8af85da95e69d2be7e367cf2431408e108bc3095/pydantic-1.10.26-py3-none-any.whl", hash = "sha256:c43ad70dc3ce7787543d563792426a16fd7895e14be4b194b5665e36459dd917", size = 166975, upload-time = "2025-12-18T15:47:44.927Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2'", + "python_full_version < '3.10' and extra != 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-18-runloop-api-client-pydantic-v1' and extra != 'group-18-runloop-api-client-pydantic-v2'", + "python_full_version < '3.10' and extra != 'group-18-runloop-api-client-pydantic-v1' and extra != 'group-18-runloop-api-client-pydantic-v2'", +] +dependencies = [ + { name = "annotated-types", marker = "extra == 'group-18-runloop-api-client-pydantic-v2' or extra != 'group-18-runloop-api-client-pydantic-v1'" }, + { name = "pydantic-core", marker = "extra == 'group-18-runloop-api-client-pydantic-v2' or extra != 'group-18-runloop-api-client-pydantic-v1'" }, + { name = "typing-extensions", marker = "extra == 'group-18-runloop-api-client-pydantic-v2' or extra != 'group-18-runloop-api-client-pydantic-v1'" }, + { name = "typing-inspection", marker = "extra == 'group-18-runloop-api-client-pydantic-v2' or extra != 'group-18-runloop-api-client-pydantic-v1'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "extra == 'group-18-runloop-api-client-pydantic-v2' or extra != 'group-18-runloop-api-client-pydantic-v1'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/54/db/160dffb57ed9a3705c4cbcbff0ac03bdae45f1ca7d58ab74645550df3fbd/pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf", size = 2107999, upload-time = "2025-11-04T13:42:03.885Z" }, + { url = "https://files.pythonhosted.org/packages/a3/7d/88e7de946f60d9263cc84819f32513520b85c0f8322f9b8f6e4afc938383/pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5", size = 1929745, upload-time = "2025-11-04T13:42:06.075Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c2/aef51e5b283780e85e99ff19db0f05842d2d4a8a8cd15e63b0280029b08f/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d", size = 1920220, upload-time = "2025-11-04T13:42:08.457Z" }, + { url = "https://files.pythonhosted.org/packages/c7/97/492ab10f9ac8695cd76b2fdb24e9e61f394051df71594e9bcc891c9f586e/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60", size = 2067296, upload-time = "2025-11-04T13:42:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/ec/23/984149650e5269c59a2a4c41d234a9570adc68ab29981825cfaf4cfad8f4/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82", size = 2231548, upload-time = "2025-11-04T13:42:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/71/0c/85bcbb885b9732c28bec67a222dbed5ed2d77baee1f8bba2002e8cd00c5c/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5", size = 2362571, upload-time = "2025-11-04T13:42:16.208Z" }, + { url = "https://files.pythonhosted.org/packages/c0/4a/412d2048be12c334003e9b823a3fa3d038e46cc2d64dd8aab50b31b65499/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3", size = 2068175, upload-time = "2025-11-04T13:42:18.911Z" }, + { url = "https://files.pythonhosted.org/packages/73/f4/c58b6a776b502d0a5540ad02e232514285513572060f0d78f7832ca3c98b/pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425", size = 2177203, upload-time = "2025-11-04T13:42:22.578Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ae/f06ea4c7e7a9eead3d165e7623cd2ea0cb788e277e4f935af63fc98fa4e6/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504", size = 2148191, upload-time = "2025-11-04T13:42:24.89Z" }, + { url = "https://files.pythonhosted.org/packages/c1/57/25a11dcdc656bf5f8b05902c3c2934ac3ea296257cc4a3f79a6319e61856/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5", size = 2343907, upload-time = "2025-11-04T13:42:27.683Z" }, + { url = "https://files.pythonhosted.org/packages/96/82/e33d5f4933d7a03327c0c43c65d575e5919d4974ffc026bc917a5f7b9f61/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3", size = 2322174, upload-time = "2025-11-04T13:42:30.776Z" }, + { url = "https://files.pythonhosted.org/packages/81/45/4091be67ce9f469e81656f880f3506f6a5624121ec5eb3eab37d7581897d/pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460", size = 1990353, upload-time = "2025-11-04T13:42:33.111Z" }, + { url = "https://files.pythonhosted.org/packages/44/8a/a98aede18db6e9cd5d66bcacd8a409fcf8134204cdede2e7de35c5a2c5ef/pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b", size = 2015698, upload-time = "2025-11-04T13:42:35.484Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyright" +version = "1.1.399" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/9d/d91d5f6d26b2db95476fefc772e2b9a16d54c6bd0ea6bb5c1b6d635ab8b4/pyright-1.1.399.tar.gz", hash = "sha256:439035d707a36c3d1b443aec980bc37053fbda88158eded24b8eedcf1c7b7a1b", size = 3856954, upload-time = "2025-04-10T04:40:25.703Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/b5/380380c9e7a534cb1783c70c3e8ac6d1193c599650a55838d0557586796e/pyright-1.1.399-py3-none-any.whl", hash = "sha256:55f9a875ddf23c9698f24208c764465ffdfd38be6265f7faf9a176e1dc549f3b", size = 5592584, upload-time = "2025-04-10T04:40:23.502Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "(python_full_version < '3.10' and sys_platform == 'win32') or (python_full_version >= '3.10' and extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2') or (sys_platform != 'win32' and extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, + { name = "packaging", marker = "python_full_version < '3.10' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, + { name = "pluggy", marker = "python_full_version < '3.10' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, + { name = "pygments", marker = "python_full_version < '3.10' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, + { name = "tomli", marker = "python_full_version < '3.10' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-18-runloop-api-client-pydantic-v1' and extra != 'group-18-runloop-api-client-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-18-runloop-api-client-pydantic-v1' and extra != 'group-18-runloop-api-client-pydantic-v2'", +] +dependencies = [ + { name = "colorama", marker = "(python_full_version >= '3.10' and sys_platform == 'win32') or (python_full_version < '3.10' and extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2') or (sys_platform != 'win32' and extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, + { name = "packaging", marker = "python_full_version >= '3.10' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, + { name = "pluggy", marker = "python_full_version >= '3.10' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, + { name = "pygments", marker = "python_full_version >= '3.10' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, + { name = "tomli", marker = "python_full_version == '3.10.*' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.10' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, + { name = "typing-extensions", marker = "python_full_version < '3.10' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095, upload-time = "2025-09-12T07:33:52.639Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-18-runloop-api-client-pydantic-v1' and extra != 'group-18-runloop-api-client-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-18-runloop-api-client-pydantic-v1' and extra != 'group-18-runloop-api-client-pydantic-v2'", +] +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version == '3.10.*' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, + { name = "typing-extensions", marker = "(python_full_version >= '3.10' and python_full_version < '3.13') or (python_full_version < '3.10' and extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2') or (python_full_version >= '3.13' and extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six", marker = "python_full_version < '3.10' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "respx" +version = "0.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/7c/96bd0bc759cf009675ad1ee1f96535edcb11e9666b985717eb8c87192a95/respx-0.22.0.tar.gz", hash = "sha256:3c8924caa2a50bd71aefc07aa812f2466ff489f1848c96e954a5362d17095d91", size = 28439, upload-time = "2024-12-19T22:33:59.374Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/67/afbb0978d5399bc9ea200f1d4489a23c9a1dad4eee6376242b8182389c79/respx-0.22.0-py2.py3-none-any.whl", hash = "sha256:631128d4c9aba15e56903fb5f66fb1eff412ce28dd387ca3a81339e52dbd3ad0", size = 25127, upload-time = "2024-12-19T22:33:57.837Z" }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, + { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/0a/1914efb7903174b381ee2ffeebb4253e729de57f114e63595114c8ca451f/ruff-0.14.13.tar.gz", hash = "sha256:83cd6c0763190784b99650a20fec7633c59f6ebe41c5cc9d45ee42749563ad47", size = 6059504, upload-time = "2026-01-15T20:15:16.918Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/ae/0deefbc65ca74b0ab1fd3917f94dc3b398233346a74b8bbb0a916a1a6bf6/ruff-0.14.13-py3-none-linux_armv6l.whl", hash = "sha256:76f62c62cd37c276cb03a275b198c7c15bd1d60c989f944db08a8c1c2dbec18b", size = 13062418, upload-time = "2026-01-15T20:14:50.779Z" }, + { url = "https://files.pythonhosted.org/packages/47/df/5916604faa530a97a3c154c62a81cb6b735c0cb05d1e26d5ad0f0c8ac48a/ruff-0.14.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914a8023ece0528d5cc33f5a684f5f38199bbb566a04815c2c211d8f40b5d0ed", size = 13442344, upload-time = "2026-01-15T20:15:07.94Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f3/e0e694dd69163c3a1671e102aa574a50357536f18a33375050334d5cd517/ruff-0.14.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d24899478c35ebfa730597a4a775d430ad0d5631b8647a3ab368c29b7e7bd063", size = 12354720, upload-time = "2026-01-15T20:15:09.854Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e8/67f5fcbbaee25e8fc3b56cc33e9892eca7ffe09f773c8e5907757a7e3bdb/ruff-0.14.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aaf3870f14d925bbaf18b8a2347ee0ae7d95a2e490e4d4aea6813ed15ebc80e", size = 12774493, upload-time = "2026-01-15T20:15:20.908Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ce/d2e9cb510870b52a9565d885c0d7668cc050e30fa2c8ac3fb1fda15c083d/ruff-0.14.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac5b7f63dd3b27cc811850f5ffd8fff845b00ad70e60b043aabf8d6ecc304e09", size = 12815174, upload-time = "2026-01-15T20:15:05.74Z" }, + { url = "https://files.pythonhosted.org/packages/88/00/c38e5da58beebcf4fa32d0ddd993b63dfacefd02ab7922614231330845bf/ruff-0.14.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2b1097750d90ba82ce4ba676e85230a0ed694178ca5e61aa9b459970b3eb9", size = 13680909, upload-time = "2026-01-15T20:15:14.537Z" }, + { url = "https://files.pythonhosted.org/packages/61/61/cd37c9dd5bd0a3099ba79b2a5899ad417d8f3b04038810b0501a80814fd7/ruff-0.14.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d0bf87705acbbcb8d4c24b2d77fbb73d40210a95c3903b443cd9e30824a5032", size = 15144215, upload-time = "2026-01-15T20:15:22.886Z" }, + { url = "https://files.pythonhosted.org/packages/56/8a/85502d7edbf98c2df7b8876f316c0157359165e16cdf98507c65c8d07d3d/ruff-0.14.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3eb5da8e2c9e9f13431032fdcbe7681de9ceda5835efee3269417c13f1fed5c", size = 14706067, upload-time = "2026-01-15T20:14:48.271Z" }, + { url = "https://files.pythonhosted.org/packages/7e/2f/de0df127feb2ee8c1e54354dc1179b4a23798f0866019528c938ba439aca/ruff-0.14.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:642442b42957093811cd8d2140dfadd19c7417030a7a68cf8d51fcdd5f217427", size = 14133916, upload-time = "2026-01-15T20:14:57.357Z" }, + { url = "https://files.pythonhosted.org/packages/0d/77/9b99686bb9fe07a757c82f6f95e555c7a47801a9305576a9c67e0a31d280/ruff-0.14.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4acdf009f32b46f6e8864af19cbf6841eaaed8638e65c8dac845aea0d703c841", size = 13859207, upload-time = "2026-01-15T20:14:55.111Z" }, + { url = "https://files.pythonhosted.org/packages/7d/46/2bdcb34a87a179a4d23022d818c1c236cb40e477faf0d7c9afb6813e5876/ruff-0.14.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:591a7f68860ea4e003917d19b5c4f5ac39ff558f162dc753a2c5de897fd5502c", size = 14043686, upload-time = "2026-01-15T20:14:52.841Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a9/5c6a4f56a0512c691cf143371bcf60505ed0f0860f24a85da8bd123b2bf1/ruff-0.14.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:774c77e841cc6e046fc3e91623ce0903d1cd07e3a36b1a9fe79b81dab3de506b", size = 12663837, upload-time = "2026-01-15T20:15:18.921Z" }, + { url = "https://files.pythonhosted.org/packages/fe/bb/b920016ece7651fa7fcd335d9d199306665486694d4361547ccb19394c44/ruff-0.14.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:61f4e40077a1248436772bb6512db5fc4457fe4c49e7a94ea7c5088655dd21ae", size = 12805867, upload-time = "2026-01-15T20:14:59.272Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b3/0bd909851e5696cd21e32a8fc25727e5f58f1934b3596975503e6e85415c/ruff-0.14.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6d02f1428357fae9e98ac7aa94b7e966fd24151088510d32cf6f902d6c09235e", size = 13208528, upload-time = "2026-01-15T20:15:03.732Z" }, + { url = "https://files.pythonhosted.org/packages/3b/3b/e2d94cb613f6bbd5155a75cbe072813756363eba46a3f2177a1fcd0cd670/ruff-0.14.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e399341472ce15237be0c0ae5fbceca4b04cd9bebab1a2b2c979e015455d8f0c", size = 13929242, upload-time = "2026-01-15T20:15:11.918Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c5/abd840d4132fd51a12f594934af5eba1d5d27298a6f5b5d6c3be45301caf/ruff-0.14.13-py3-none-win32.whl", hash = "sha256:ef720f529aec113968b45dfdb838ac8934e519711da53a0456038a0efecbd680", size = 12919024, upload-time = "2026-01-15T20:14:43.647Z" }, + { url = "https://files.pythonhosted.org/packages/c2/55/6384b0b8ce731b6e2ade2b5449bf07c0e4c31e8a2e68ea65b3bafadcecc5/ruff-0.14.13-py3-none-win_amd64.whl", hash = "sha256:6070bd026e409734b9257e03e3ef18c6e1a216f0435c6751d7a8ec69cb59abef", size = 14097887, upload-time = "2026-01-15T20:15:01.48Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/7348090988095e4e39560cfc2f7555b1b2a7357deba19167b600fdf5215d/ruff-0.14.13-py3-none-win_arm64.whl", hash = "sha256:7ab819e14f1ad9fe39f246cfcc435880ef7a9390d81a2b6ac7e01039083dd247", size = 13080224, upload-time = "2026-01-15T20:14:45.853Z" }, +] + +[[package]] +name = "runloop-api-client" +version = "1.11.0" +source = { editable = "." } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "pydantic", version = "1.10.26", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-18-runloop-api-client-pydantic-v1'" }, + { name = "pydantic", version = "2.12.5", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-18-runloop-api-client-pydantic-v2' or extra != 'group-18-runloop-api-client-pydantic-v1'" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] + +[package.optional-dependencies] +aiohttp = [ + { name = "aiohttp" }, + { name = "httpx-aiohttp" }, +] + +[package.dev-dependencies] +dev = [ + { name = "dirty-equals" }, + { name = "importlib-metadata" }, + { name = "mypy" }, + { name = "pyright" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, + { name = "pytest-asyncio", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, + { name = "pytest-asyncio", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, + { name = "pytest-xdist" }, + { name = "respx" }, + { name = "rich" }, + { name = "ruff" }, + { name = "time-machine", version = "2.19.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, + { name = "time-machine", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, +] +pydantic-v1 = [ + { name = "pydantic", version = "1.10.26", source = { registry = "https://pypi.org/simple" } }, +] +pydantic-v2 = [ + { name = "pydantic", version = "2.12.5", source = { registry = "https://pypi.org/simple" } }, +] + +[package.metadata] +requires-dist = [ + { name = "aiohttp", marker = "extra == 'aiohttp'" }, + { name = "anyio", specifier = ">=3.5.0,<5" }, + { name = "distro", specifier = ">=1.7.0,<2" }, + { name = "httpx", specifier = ">=0.23.0,<1" }, + { name = "httpx-aiohttp", marker = "extra == 'aiohttp'", specifier = ">=0.1.9" }, + { name = "pydantic", specifier = ">=1.9.0,<3" }, + { name = "sniffio" }, + { name = "typing-extensions", specifier = ">=4.10,<5" }, +] +provides-extras = ["aiohttp"] + +[package.metadata.requires-dev] +dev = [ + { name = "dirty-equals", specifier = ">=0.6.0" }, + { name = "importlib-metadata", specifier = ">=6.7.0" }, + { name = "mypy", specifier = "==1.17" }, + { name = "pyright", specifier = "==1.1.399" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-xdist", specifier = ">=3.6.1" }, + { name = "respx" }, + { name = "rich", specifier = ">=13.7.1" }, + { name = "ruff" }, + { name = "time-machine" }, +] +pydantic-v1 = [{ name = "pydantic", specifier = ">=1.9.0,<2" }] +pydantic-v2 = [ + { name = "pydantic", marker = "python_full_version < '3.14'", specifier = "~=2.0" }, + { name = "pydantic", marker = "python_full_version >= '3.14'", specifier = "~=2.12" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "time-machine" +version = "2.19.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "python-dateutil", marker = "python_full_version < '3.10' or (extra == 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/a4/1b5fdd165f61b67f445fac2a7feb0c655118edef429cd09ff5a8067f7f1d/time_machine-2.19.0.tar.gz", hash = "sha256:7c5065a8b3f2bbb449422c66ef71d114d3f909c276a6469642ecfffb6a0fcd29", size = 14576, upload-time = "2025-08-19T17:22:08.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/8f/19125611ebbcb3a14da14cd982b9eb4573e2733db60c9f1fbf6a39534f40/time_machine-2.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b5169018ef47206997b46086ce01881cd3a4666fd2998c9d76a87858ca3e49e9", size = 19659, upload-time = "2025-08-19T17:20:30.062Z" }, + { url = "https://files.pythonhosted.org/packages/74/da/9b0a928321e7822a3ff96dbd1eae089883848e30e9e1b149b85fb96ba56b/time_machine-2.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85bb7ed440fccf6f6d0c8f7d68d849e7c3d1f771d5e0b2cdf871fa6561da569f", size = 15157, upload-time = "2025-08-19T17:20:31.931Z" }, + { url = "https://files.pythonhosted.org/packages/36/ff/d7e943422038f5f2161fe2c2d791e64a45be691ef946020b20f3a6efc4d4/time_machine-2.19.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a3b12028af1cdc09ccd595be2168b7b26f206c1e190090b048598fbe278beb8e", size = 32860, upload-time = "2025-08-19T17:20:33.241Z" }, + { url = "https://files.pythonhosted.org/packages/fc/80/2b0f1070ed9808ee7da7a6da62a4a0b776957cb4d861578348f86446e778/time_machine-2.19.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c261f073086cf081d1443cbf7684148c662659d3d139d06b772bfe3fe7cc71a6", size = 34510, upload-time = "2025-08-19T17:20:34.221Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b4/48038691c8d89924b36c83335a73adeeb68c884f5a1da08a5b17b8a956f3/time_machine-2.19.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:011954d951230a9f1079f22b39ed1a3a9abb50ee297dfb8c557c46351659d94d", size = 36204, upload-time = "2025-08-19T17:20:35.163Z" }, + { url = "https://files.pythonhosted.org/packages/37/2e/60e8adb541df195e83cb74b720b2cfb1f22ed99c5a7f8abf2a9ab3442cb5/time_machine-2.19.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b0f83308b29c7872006803f2e77318874eb84d0654f2afe0e48e3822e7a2e39b", size = 34936, upload-time = "2025-08-19T17:20:36.61Z" }, + { url = "https://files.pythonhosted.org/packages/5e/72/e8cee59c6cd99dd3b25b8001a0253e779a286aa8f44d5b40777cbd66210b/time_machine-2.19.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:39733ef844e2984620ec9382a42d00cccc4757d75a5dd572be8c2572e86e50b9", size = 32932, upload-time = "2025-08-19T17:20:37.901Z" }, + { url = "https://files.pythonhosted.org/packages/2c/eb/83f300d93c1504965d944e03679f1c943a923bce2d0fdfadef0e2e22cc13/time_machine-2.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f8db99f6334432e9ffbf00c215caf2ae9773f17cec08304d77e9e90febc3507b", size = 34010, upload-time = "2025-08-19T17:20:39.202Z" }, + { url = "https://files.pythonhosted.org/packages/e1/77/f35f2500e04daac5033a22fbfd17e68467822b8406ee77966bf222ccaa26/time_machine-2.19.0-cp310-cp310-win32.whl", hash = "sha256:72bf66cd19e27ffd26516b9cbe676d50c2e0b026153289765dfe0cf406708128", size = 17121, upload-time = "2025-08-19T17:20:40.108Z" }, + { url = "https://files.pythonhosted.org/packages/db/df/32d3e0404be1760a64a44caab2af34b07e952bfe00a23134fea9ddba3e8a/time_machine-2.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:46f1c945934ce3d6b4f388b8e581fce7f87ec891ea90d7128e19520e434f96f0", size = 17957, upload-time = "2025-08-19T17:20:41.079Z" }, + { url = "https://files.pythonhosted.org/packages/66/df/598a71a1afb4b509a4587273b76590b16d9110a3e9106f01eedc68d02bb2/time_machine-2.19.0-cp310-cp310-win_arm64.whl", hash = "sha256:fb4897c7a5120a4fd03f0670f332d83b7e55645886cd8864a71944c4c2e5b35b", size = 16821, upload-time = "2025-08-19T17:20:41.967Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ed/4815ebcc9b6c14273f692b9be38a9b09eae52a7e532407cc61a51912b121/time_machine-2.19.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5ee91664880434d98e41585c3446dac7180ec408c786347451ddfca110d19296", size = 19342, upload-time = "2025-08-19T17:20:43.207Z" }, + { url = "https://files.pythonhosted.org/packages/ee/08/154cce8b11b60d8238b0b751b8901d369999f4e8f7c3a5f917caa5d95b0b/time_machine-2.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ed3732b83a893d1c7b8cabde762968b4dc5680ee0d305b3ecca9bb516f4e3862", size = 14978, upload-time = "2025-08-19T17:20:44.134Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b7/b689d8c8eeca7af375cfcd64973e49e83aa817cc00f80f98548d42c0eb50/time_machine-2.19.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6ba0303e9cc9f7f947e344f501e26bedfb68fab521e3c2729d370f4f332d2d55", size = 30964, upload-time = "2025-08-19T17:20:45.366Z" }, + { url = "https://files.pythonhosted.org/packages/80/91/38bf9c79674e95ce32e23c267055f281dff651eec77ed32a677db3dc011a/time_machine-2.19.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2851825b524a988ee459c37c1c26bdfaa7eff78194efb2b562ea497a6f375b0a", size = 32606, upload-time = "2025-08-19T17:20:46.693Z" }, + { url = "https://files.pythonhosted.org/packages/19/4a/e9222d85d4de68975a5e799f539a9d32f3a134a9101fca0a61fa6aa33d68/time_machine-2.19.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68d32b09ecfd7fef59255c091e8e7c24dd117f882c4880b5c7ab8c5c32a98f89", size = 34405, upload-time = "2025-08-19T17:20:48.032Z" }, + { url = "https://files.pythonhosted.org/packages/14/e2/09480d608d42d6876f9ff74593cfc9197a7eb2c31381a74fb2b145575b65/time_machine-2.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60c46ab527bf2fa144b530f639cc9e12803524c9e1f111dc8c8f493bb6586eeb", size = 33181, upload-time = "2025-08-19T17:20:48.937Z" }, + { url = "https://files.pythonhosted.org/packages/84/64/f9359e000fad32d9066305c48abc527241d608bcdf77c19d67d66e268455/time_machine-2.19.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:56f26ab9f0201c453d18fe76bb7d1cf05fe58c1b9d9cb0c7d243d05132e01292", size = 31036, upload-time = "2025-08-19T17:20:50.276Z" }, + { url = "https://files.pythonhosted.org/packages/71/0d/fab2aacec71e3e482bd7fce0589381f9414a4a97f8766bddad04ad047b7b/time_machine-2.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6c806cf3c1185baa1d807b7f51bed0db7a6506832c961d5d1b4c94c775749bc0", size = 32145, upload-time = "2025-08-19T17:20:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/44/fb/faeba2405fb27553f7b28db441a500e2064ffdb2dcba001ee315fdd2c121/time_machine-2.19.0-cp311-cp311-win32.whl", hash = "sha256:b30039dfd89855c12138095bee39c540b4633cbc3684580d684ef67a99a91587", size = 17004, upload-time = "2025-08-19T17:20:52.38Z" }, + { url = "https://files.pythonhosted.org/packages/2f/84/87e483d660ca669426192969280366635c845c3154a9fe750be546ed3afc/time_machine-2.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:13ed8b34430f1de79905877f5600adffa626793ab4546a70a99fb72c6a3350d8", size = 17822, upload-time = "2025-08-19T17:20:53.348Z" }, + { url = "https://files.pythonhosted.org/packages/41/f4/ebf7bbf5047854a528adaf54a5e8780bc5f7f0104c298ab44566a3053bf8/time_machine-2.19.0-cp311-cp311-win_arm64.whl", hash = "sha256:cc29a50a0257d8750b08056b66d7225daab47606832dea1a69e8b017323bf511", size = 16680, upload-time = "2025-08-19T17:20:54.26Z" }, + { url = "https://files.pythonhosted.org/packages/9b/aa/7e00614d339e4d687f6e96e312a1566022528427d237ec639df66c4547bc/time_machine-2.19.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c85cf437dc3c07429456d8d6670ac90ecbd8241dcd0fbf03e8db2800576f91ff", size = 19308, upload-time = "2025-08-19T17:20:55.25Z" }, + { url = "https://files.pythonhosted.org/packages/ab/3c/bde3c757394f5bca2fbc1528d4117960a26c38f9b160bf471b38d2378d8f/time_machine-2.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d9238897e8ef54acdf59f5dff16f59ca0720e7c02d820c56b4397c11db5d3eb9", size = 15019, upload-time = "2025-08-19T17:20:56.204Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e0/8ca916dd918018352d377f1f5226ee071cfbeb7dbbde2b03d14a411ac2b1/time_machine-2.19.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e312c7d5d6bfffb96c6a7b39ff29e3046de100d7efaa3c01552654cfbd08f14c", size = 33079, upload-time = "2025-08-19T17:20:57.166Z" }, + { url = "https://files.pythonhosted.org/packages/48/69/184a0209f02dd0cb5e01e8d13cd4c97a5f389c4e3d09b95160dd676ad1e7/time_machine-2.19.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:714c40b2c90d1c57cc403382d5a9cf16e504cb525bfe9650095317da3c3d62b5", size = 34925, upload-time = "2025-08-19T17:20:58.117Z" }, + { url = "https://files.pythonhosted.org/packages/43/42/4bbf4309e8e57cea1086eb99052d97ff6ddecc1ab6a3b07aa4512f8bf963/time_machine-2.19.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2eaa1c675d500dc3ccae19e9fb1feff84458a68c132bbea47a80cc3dd2df7072", size = 36384, upload-time = "2025-08-19T17:20:59.108Z" }, + { url = "https://files.pythonhosted.org/packages/b1/af/9f510dc1719157348c1a2e87423aed406589070b54b503cb237d9bf3a4fe/time_machine-2.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e77a414e9597988af53b2b2e67242c9d2f409769df0d264b6d06fda8ca3360d4", size = 34881, upload-time = "2025-08-19T17:21:00.116Z" }, + { url = "https://files.pythonhosted.org/packages/ca/28/61764a635c70cc76c76ba582dfdc1a84834cddaeb96789023af5214426b2/time_machine-2.19.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cd93996970e11c382b04d4937c3cd0b0167adeef14725ece35aae88d8a01733c", size = 32931, upload-time = "2025-08-19T17:21:01.095Z" }, + { url = "https://files.pythonhosted.org/packages/b6/e0/f028d93b266e6ade8aca5851f76ebbc605b2905cdc29981a2943b43e1a6c/time_machine-2.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8e20a6d8d6e23174bd7e931e134d9610b136db460b249d07e84ecdad029ec352", size = 34241, upload-time = "2025-08-19T17:21:02.052Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a6/36d1950ed1d3f613158024cf1dcc73db1d9ef0b9117cf51ef2e37dc06499/time_machine-2.19.0-cp312-cp312-win32.whl", hash = "sha256:95afc9bc65228b27be80c2756799c20b8eb97c4ef382a9b762b6d7888bc84099", size = 17021, upload-time = "2025-08-19T17:21:03.374Z" }, + { url = "https://files.pythonhosted.org/packages/b1/0d/e2dce93355abda3cac69e77fe96566757e98b8fe7fdcbddce89c9ced3f5f/time_machine-2.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:e84909af950e2448f4e2562ea5759c946248c99ab380d2b47d79b62bd76fa236", size = 17857, upload-time = "2025-08-19T17:21:04.331Z" }, + { url = "https://files.pythonhosted.org/packages/eb/28/50ae6fb83b7feeeca7a461c0dc156cf7ef5e6ef594a600d06634fde6a2cb/time_machine-2.19.0-cp312-cp312-win_arm64.whl", hash = "sha256:0390a1ea9fa7e9d772a39b7c61b34fdcca80eb9ffac339cc0441c6c714c81470", size = 16677, upload-time = "2025-08-19T17:21:05.39Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b8/24ebce67aa531bae2cbe164bb3f4abc6467dc31f3aead35e77f5a075ea3e/time_machine-2.19.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5e172866753e6041d3b29f3037dc47c20525176a494a71bbd0998dfdc4f11f2f", size = 19373, upload-time = "2025-08-19T17:21:06.701Z" }, + { url = "https://files.pythonhosted.org/packages/53/a5/c9a5240fd2f845d3ff9fa26f8c8eaa29f7239af9d65007e61d212250f15b/time_machine-2.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f70f68379bd6f542ae6775cce9a4fa3dcc20bf7959c42eaef871c14469e18097", size = 15056, upload-time = "2025-08-19T17:21:07.667Z" }, + { url = "https://files.pythonhosted.org/packages/b9/92/66cce5d2fb2a5e68459aca85fd18a7e2d216f725988940cd83f96630f2f1/time_machine-2.19.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e69e0b0f694728a00e72891ef8dd00c7542952cb1c87237db594b6b27d504a96", size = 33172, upload-time = "2025-08-19T17:21:08.619Z" }, + { url = "https://files.pythonhosted.org/packages/ae/20/b499e9ab4364cd466016c33dcdf4f56629ca4c20b865bd4196d229f31d92/time_machine-2.19.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3ae0a8b869574301ec5637e32c270c7384cca5cd6e230f07af9d29271a7fa293", size = 35042, upload-time = "2025-08-19T17:21:09.622Z" }, + { url = "https://files.pythonhosted.org/packages/41/32/b252d3d32791eb16c07d553c820dbc33d9c7fa771de3d1c602190bded2b7/time_machine-2.19.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:554e4317de90e2f7605ff80d153c8bb56b38c0d0c0279feb17e799521e987b8c", size = 36535, upload-time = "2025-08-19T17:21:10.571Z" }, + { url = "https://files.pythonhosted.org/packages/98/cf/4d0470062b9742e1b040ab81bad04d1a5d1de09806507bb6188989cfa1a7/time_machine-2.19.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6567a5ec5538ed550539ac29be11b3cb36af1f9894e2a72940cba0292cc7c3c9", size = 34945, upload-time = "2025-08-19T17:21:11.538Z" }, + { url = "https://files.pythonhosted.org/packages/24/71/2f741b29d98b1c18f6777a32236497c3d3264b6077e431cea4695684c8a1/time_machine-2.19.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82e9ffe8dfff07b0d810a2ad015a82cd78c6a237f6c7cf185fa7f747a3256f8a", size = 33014, upload-time = "2025-08-19T17:21:12.858Z" }, + { url = "https://files.pythonhosted.org/packages/e8/83/ca8dba6106562843fd99f672e5aaf95badbc10f4f13f7cfe8d8640a7019d/time_machine-2.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7e1c4e578cdd69b3531d8dd3fbcb92a0cd879dadb912ee37af99c3a9e3c0d285", size = 34350, upload-time = "2025-08-19T17:21:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/21/7f/34fe540450e18d0a993240100e4b86e8d03d831b92af8bb6ddb2662dc6fc/time_machine-2.19.0-cp313-cp313-win32.whl", hash = "sha256:72dbd4cbc3d96dec9dd281ddfbb513982102776b63e4e039f83afb244802a9e5", size = 17047, upload-time = "2025-08-19T17:21:14.874Z" }, + { url = "https://files.pythonhosted.org/packages/bf/5d/c8be73df82c7ebe7cd133279670e89b8b110af3ce1412c551caa9d08e625/time_machine-2.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:e17e3e089ac95f9a145ce07ff615e3c85674f7de36f2d92aaf588493a23ffb4b", size = 17868, upload-time = "2025-08-19T17:21:15.819Z" }, + { url = "https://files.pythonhosted.org/packages/92/13/2dfd3b8fb285308f61cd7aa9bfa96f46ddf916e3549a0f0afd094c556599/time_machine-2.19.0-cp313-cp313-win_arm64.whl", hash = "sha256:149072aff8e3690e14f4916103d898ea0d5d9c95531b6aa0995251c299533f7b", size = 16710, upload-time = "2025-08-19T17:21:16.748Z" }, + { url = "https://files.pythonhosted.org/packages/05/c1/deebb361727d2c5790f9d4d874be1b19afd41f4375581df465e6718b46a2/time_machine-2.19.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f3589fee1ed0ab6ee424a55b0ea1ec694c4ba64cc26895bcd7d99f3d1bc6a28a", size = 20053, upload-time = "2025-08-19T17:21:17.704Z" }, + { url = "https://files.pythonhosted.org/packages/45/e8/fe3376951e6118d8ec1d1f94066a169b791424fe4a26c7dfc069b153ee08/time_machine-2.19.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7887e85275c4975fe54df03dcdd5f38bd36be973adc68a8c77e17441c3b443d6", size = 15423, upload-time = "2025-08-19T17:21:18.668Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c7/f88d95cd1a87c650cf3749b4d64afdaf580297aa18ad7f4b44ec9d252dfc/time_machine-2.19.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ce0be294c209928563fcce1c587963e60ec803436cf1e181acd5bc1e425d554b", size = 39630, upload-time = "2025-08-19T17:21:19.645Z" }, + { url = "https://files.pythonhosted.org/packages/cc/5d/65a5c48a65357e56ec6f032972e4abd1c02d4fca4b0717a3aaefd19014d4/time_machine-2.19.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a62fd1ab380012c86f4c042010418ed45eb31604f4bf4453e17c9fa60bc56a29", size = 41242, upload-time = "2025-08-19T17:21:20.979Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f9/fe5209e1615fde0a8cad6c4e857157b150333ed1fe31a7632b08cfe0ebdd/time_machine-2.19.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b25ec853a4530a5800731257f93206b12cbdee85ede964ebf8011b66086a7914", size = 44278, upload-time = "2025-08-19T17:21:21.984Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3a/a5e5fe9c5d614cde0a9387ff35e8dfd12c5ef6384e4c1a21b04e6e0b905d/time_machine-2.19.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a430e4d0e0556f021a9c78e9b9f68e5e8910bdace4aa34ed4d1a73e239ed9384", size = 42321, upload-time = "2025-08-19T17:21:23.755Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c5/56eca774e9162bc1ce59111d2bd69140dc8908c9478c92ec7bd15d547600/time_machine-2.19.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2415b7495ec4364c8067071e964fbadfe746dd4cdb43983f2f0bd6ebed13315c", size = 39270, upload-time = "2025-08-19T17:21:26.009Z" }, + { url = "https://files.pythonhosted.org/packages/9b/69/5dd0c420667578169a12acc8c8fd7452e8cfb181e41c9b4ac7e88fa36686/time_machine-2.19.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dbfc6b90c10f288594e1bf89a728a98cc0030791fd73541bbdc6b090aff83143", size = 40193, upload-time = "2025-08-19T17:21:27.054Z" }, + { url = "https://files.pythonhosted.org/packages/75/a7/de974d421bd55c9355583427c2a38fb0237bb5fd6614af492ba89dacb2f9/time_machine-2.19.0-cp313-cp313t-win32.whl", hash = "sha256:16f5d81f650c0a4d117ab08036dc30b5f8b262e11a4a0becc458e7f1c011b228", size = 17542, upload-time = "2025-08-19T17:21:28.674Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/aa0d05becd5d06ae8d3f16d657dc8cc9400c8d79aef80299de196467ff12/time_machine-2.19.0-cp313-cp313t-win_amd64.whl", hash = "sha256:645699616ec14e147094f601e6ab9553ff6cea37fad9c42720a6d7ed04bcd5dc", size = 18703, upload-time = "2025-08-19T17:21:29.663Z" }, + { url = "https://files.pythonhosted.org/packages/1f/c0/f785a4c7c73aa176510f7c48b84b49c26be84af0d534deb222e0327f750e/time_machine-2.19.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b32daa965d13237536ea3afaa5ad61ade2b2d9314bc3a20196a0d2e1d7b57c6a", size = 17020, upload-time = "2025-08-19T17:21:30.653Z" }, + { url = "https://files.pythonhosted.org/packages/ed/97/c5fb51def06c0b2b6735332ad118ab35b4d9b85368792e5b638e99b1b686/time_machine-2.19.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:31cb43c8fd2d961f31bed0ff4e0026964d2b35e5de9e0fabbfecf756906d3612", size = 19360, upload-time = "2025-08-19T17:21:31.94Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4e/2d795f7d6b7f5205ffe737a05bb1cf19d8038233b797062b2ef412b8512b/time_machine-2.19.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:bdf481a75afc6bff3e520db594501975b652f7def21cd1de6aa971d35ba644e6", size = 15033, upload-time = "2025-08-19T17:21:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/dd/32/9bad501e360b4e758c58fae616ca5f8c7ad974b343f2463a15b2bf77a366/time_machine-2.19.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:00bee4bb950ac6a08d62af78e4da0cf2b4fc2abf0de2320d0431bf610db06e7c", size = 33379, upload-time = "2025-08-19T17:21:33.925Z" }, + { url = "https://files.pythonhosted.org/packages/a3/45/eda0ca4d793dfd162478d6163759b1c6ce7f6e61daa7fd7d62b31f21f87f/time_machine-2.19.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9f02199490906582302ce09edd32394fb393271674c75d7aa76c7a3245f16003", size = 35123, upload-time = "2025-08-19T17:21:34.945Z" }, + { url = "https://files.pythonhosted.org/packages/f0/5a/97e16325442ae5731fcaac794f0a1ef9980eff8a5491e58201d7eb814a34/time_machine-2.19.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e35726c7ba625f844c13b1fc0d4f81f394eefaee1d3a094a9093251521f2ef15", size = 36588, upload-time = "2025-08-19T17:21:35.975Z" }, + { url = "https://files.pythonhosted.org/packages/e8/9d/bf0b2ccc930cc4a316f26f1c78d3f313cd0fa13bb7480369b730a8f129db/time_machine-2.19.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:304315023999cd401ff02698870932b893369e1cfeb2248d09f6490507a92e97", size = 35013, upload-time = "2025-08-19T17:21:37.017Z" }, + { url = "https://files.pythonhosted.org/packages/f0/5a/39ac6a3078174f9715d88364871348b249631f12e76de1b862433b3f8862/time_machine-2.19.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9765d4f003f263ea8bfd90d2d15447ca4b3dfa181922cf6cf808923b02ac180a", size = 33303, upload-time = "2025-08-19T17:21:38.352Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ac/d8646baf9f95f2e792a6d7a7b35e92fca253c4a992afff801beafae0e5c2/time_machine-2.19.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7837ef3fd5911eb9b480909bb93d922737b6bdecea99dfcedb0a03807de9b2d3", size = 34440, upload-time = "2025-08-19T17:21:39.382Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8b/8b6568c5ae966d80ead03ab537be3c6acf2af06fb501c2d466a3162c6295/time_machine-2.19.0-cp314-cp314-win32.whl", hash = "sha256:4bb5bd43b1bdfac3007b920b51d8e761f024ed465cfeec63ac4296922a4ec428", size = 17162, upload-time = "2025-08-19T17:21:40.381Z" }, + { url = "https://files.pythonhosted.org/packages/46/a5/211c1ab4566eba5308b2dc001b6349e3a032e3f6afa67ca2f27ea6b27af5/time_machine-2.19.0-cp314-cp314-win_amd64.whl", hash = "sha256:f583bbd0aa8ab4a7c45a684bf636d9e042d466e30bcbae1d13e7541e2cbe7207", size = 18040, upload-time = "2025-08-19T17:21:41.363Z" }, + { url = "https://files.pythonhosted.org/packages/b8/fc/4c2fb705f6371cb83824da45a8b967514a922fc092a0ef53979334d97a70/time_machine-2.19.0-cp314-cp314-win_arm64.whl", hash = "sha256:f379c6f8a6575a8284592179cf528ce89373f060301323edcc44f1fa1d37be12", size = 16752, upload-time = "2025-08-19T17:21:42.336Z" }, + { url = "https://files.pythonhosted.org/packages/79/ab/6437d18f31c666b5116c97572a282ac2590a82a0a9867746a6647eaf4613/time_machine-2.19.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a3b8981f9c663b0906b05ab4d0ca211fae4b63b47c6ec26de5374fe56c836162", size = 20057, upload-time = "2025-08-19T17:21:43.35Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a2/e03639ec2ba7200328bbcad8a2b2b1d5fccca9cceb9481b164a1cabdcb33/time_machine-2.19.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8e9c6363893e7f52c226afbebb23e825259222d100e67dfd24c8a6d35f1a1907", size = 15430, upload-time = "2025-08-19T17:21:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ff/39e63a48e840f3e36ce24846ee51dd99c6dba635659b1750a2993771e88e/time_machine-2.19.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:206fcd6c9a6f00cac83db446ad1effc530a8cec244d2780af62db3a2d0a9871b", size = 39622, upload-time = "2025-08-19T17:21:45.821Z" }, + { url = "https://files.pythonhosted.org/packages/9a/2e/ee5ac79c4954768705801e54817c7d58e07e25a0bb227e775f501f3e2122/time_machine-2.19.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf33016a1403c123373ffaeff25e26e69d63bf2c63b6163932efed94160db7ef", size = 41235, upload-time = "2025-08-19T17:21:46.783Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3e/9af5f39525e779185c77285b8bbae15340eeeaa0afb33d458bc8b47d459b/time_machine-2.19.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9247c4bb9bbd3ff584ef4efbdec8efd9f37aa08bcfc4728bde1e489c2cb445bd", size = 44276, upload-time = "2025-08-19T17:21:47.759Z" }, + { url = "https://files.pythonhosted.org/packages/59/fe/572c7443cc27140bbeae3947279bbd4a120f9e8622253a20637f260b7813/time_machine-2.19.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:77f9bb0b86758d1f2d9352642c874946ad5815df53ef4ca22eb9d532179fe50d", size = 42330, upload-time = "2025-08-19T17:21:48.881Z" }, + { url = "https://files.pythonhosted.org/packages/cf/24/1a81c2e08ee7dae13ec8ceed27a29afa980c3d63852e42f1e023bf0faa03/time_machine-2.19.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0b529e262df3b9c449f427385f4d98250828c879168c2e00eec844439f40b370", size = 39281, upload-time = "2025-08-19T17:21:49.907Z" }, + { url = "https://files.pythonhosted.org/packages/d2/60/6f0d6e5108978ca1a2a4ffb4d1c7e176d9199bb109fd44efe2680c60b52a/time_machine-2.19.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9199246e31cdc810e5d89cb71d09144c4d745960fdb0824da4994d152aca3303", size = 40201, upload-time = "2025-08-19T17:21:50.953Z" }, + { url = "https://files.pythonhosted.org/packages/73/b9/3ea4951e8293b0643feb98c0b9a176fa822154f1810835db3f282968ab10/time_machine-2.19.0-cp314-cp314t-win32.whl", hash = "sha256:0fe81bae55b7aefc2c2a34eb552aa82e6c61a86b3353a3c70df79b9698cb02ca", size = 17743, upload-time = "2025-08-19T17:21:51.948Z" }, + { url = "https://files.pythonhosted.org/packages/e4/8b/cd802884ca8a98e2b6cdc2397d57dd12ff8a7d1481e06fc3fad3d4e7e5ff/time_machine-2.19.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7253791b8d7e7399fbeed7a8193cb01bc004242864306288797056badbdaf80b", size = 18956, upload-time = "2025-08-19T17:21:52.997Z" }, + { url = "https://files.pythonhosted.org/packages/c6/49/cabb1593896082fd55e34768029b8b0ca23c9be8b2dc127e0fc14796d33e/time_machine-2.19.0-cp314-cp314t-win_arm64.whl", hash = "sha256:536bd1ac31ab06a1522e7bf287602188f502dc19d122b1502c4f60b1e8efac79", size = 17068, upload-time = "2025-08-19T17:21:54.064Z" }, + { url = "https://files.pythonhosted.org/packages/d6/05/0608376c3167afe6cf7cdfd2b05c142ea4c42616eee9ba06d1799965806a/time_machine-2.19.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8bb00b30ec9fe56d01e9812df1ffe39f331437cef9bfaebcc81c83f7f8f8ee2", size = 19659, upload-time = "2025-08-19T17:21:55.426Z" }, + { url = "https://files.pythonhosted.org/packages/11/c4/72eb8c7b36830cf36c51d7bc2f1ac313d68881c3a58040fb6b42c4523d20/time_machine-2.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d821c60efc08a97cc11e5482798e6fd5eba5c0f22a02db246b50895dbdc0de41", size = 15153, upload-time = "2025-08-19T17:21:56.505Z" }, + { url = "https://files.pythonhosted.org/packages/89/1a/0782e1f5c8ab8809ebd992709e1bb69d67600191baa023af7a5d32023a3c/time_machine-2.19.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb051aec7b3b6e96a200d911c225901e6133ff3da11e470e24111a53bbc13637", size = 32555, upload-time = "2025-08-19T17:21:57.74Z" }, + { url = "https://files.pythonhosted.org/packages/94/b0/8ef58e2f6321851d5900ca3d18044938832c2ed42a2ac7570ca6aa29768a/time_machine-2.19.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fe59909d95a2ef5e01ce3354fdea3908404c2932c2069f00f66dff6f27e9363e", size = 34185, upload-time = "2025-08-19T17:21:59.361Z" }, + { url = "https://files.pythonhosted.org/packages/82/74/ce0c9867f788c1fb22c417ec1aae47a24117e53d51f6ff97d7c6ca5392f6/time_machine-2.19.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29e84b8682645b16eb6f9e8ec11c35324ad091841a11cf4fc3fc7f6119094c89", size = 35917, upload-time = "2025-08-19T17:22:00.421Z" }, + { url = "https://files.pythonhosted.org/packages/d2/70/6f97a8f552dbaa66feb10170b5726dab74bc531673d1ed9d6f271547e54c/time_machine-2.19.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a11f1c0e0d06023dc01614c964e256138913551d3ae6dca5148f79081156336", size = 34584, upload-time = "2025-08-19T17:22:01.447Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/cf139088ce537c15d7f03cf56ec317d3a5cfb520e30aa711ea0248d0ae8a/time_machine-2.19.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:57a235a6307c54df50e69f1906e2f199e47da91bde4b886ee05aff57fe4b6bf6", size = 32608, upload-time = "2025-08-19T17:22:02.548Z" }, + { url = "https://files.pythonhosted.org/packages/b1/17/0ec41ef7a30c6753fb226a28b74162b264b35724905ced4098f2f5076ded/time_machine-2.19.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:426aba552f7af9604adad9ef570c859af7c1081d878db78089fac159cd911b0a", size = 33686, upload-time = "2025-08-19T17:22:03.606Z" }, + { url = "https://files.pythonhosted.org/packages/b0/19/586f15159083ec84f178d494c60758c46603b00c9641b04deb63f1950128/time_machine-2.19.0-cp39-cp39-win32.whl", hash = "sha256:67772c7197a3a712d1b970ed545c6e98db73524bd90e245fd3c8fa7ad7630768", size = 17133, upload-time = "2025-08-19T17:22:04.989Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c2/bfe4b906a9fe0bf2d011534314212ed752d6b8f392c9c82f6ac63dccc5ab/time_machine-2.19.0-cp39-cp39-win_amd64.whl", hash = "sha256:011d7859089263204dc5fdf83dce7388f986fe833c9381d6106b4edfda2ebd3e", size = 17972, upload-time = "2025-08-19T17:22:06.026Z" }, + { url = "https://files.pythonhosted.org/packages/5d/73/182343eba05aa5787732aaa68f3b3feb5e40ddf86b928ae941be45646393/time_machine-2.19.0-cp39-cp39-win_arm64.whl", hash = "sha256:e1af66550fa4685434f00002808a525f176f1f92746646c0019bb86fbff48b27", size = 16820, upload-time = "2025-08-19T17:22:07.227Z" }, +] + +[[package]] +name = "time-machine" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-18-runloop-api-client-pydantic-v1' and extra == 'group-18-runloop-api-client-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-18-runloop-api-client-pydantic-v1' and extra != 'group-18-runloop-api-client-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-18-runloop-api-client-pydantic-v1' and extra != 'group-18-runloop-api-client-pydantic-v2'", +] +sdist = { url = "https://files.pythonhosted.org/packages/02/fc/37b02f6094dbb1f851145330460532176ed2f1dc70511a35828166c41e52/time_machine-3.2.0.tar.gz", hash = "sha256:a4ddd1cea17b8950e462d1805a42b20c81eb9aafc8f66b392dd5ce997e037d79", size = 14804, upload-time = "2025-12-17T23:33:02.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/31/6bf41cb4a326230518d9b76c910dfc11d4fc23444d1cbfdf2d7652bd99f4/time_machine-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:68142c070e78b62215d8029ec7394905083a4f9aacb0a2a11514ce70b5951b13", size = 19447, upload-time = "2025-12-17T23:31:30.181Z" }, + { url = "https://files.pythonhosted.org/packages/fa/14/d71ce771712e1cbfa15d8c24452225109262b16cb6caaf967e9f60662b67/time_machine-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:161bbd0648802ffdfcb4bb297ecb26b3009684a47d3a4dedb90bc549df4fa2ad", size = 15432, upload-time = "2025-12-17T23:31:31.381Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d6/dcb43a11f8029561996fad58ff9d3dc5e6d7f32b74f0745a2965d7e4b4f3/time_machine-3.2.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1359ba8c258be695ba69253bc84db882fd616fe69b426cc6056536da2c7bf68e", size = 32956, upload-time = "2025-12-17T23:31:32.469Z" }, + { url = "https://files.pythonhosted.org/packages/77/da/d802cd3c335c414f9b11b479f7459aa72df5de6485c799966cfdf8856d53/time_machine-3.2.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c85b169998ca2c24a78fb214586ec11c4cad56d9c38f55ad8326235cb481c884", size = 34556, upload-time = "2025-12-17T23:31:33.946Z" }, + { url = "https://files.pythonhosted.org/packages/85/ee/51ad553514ab0b940c7c82c6e1519dd10fd06ac07b32039a1d153ef09c88/time_machine-3.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65b9367cb8a10505bc8f67da0da514ba20fa816fc47e11f434f7c60350322b4c", size = 36101, upload-time = "2025-12-17T23:31:35.462Z" }, + { url = "https://files.pythonhosted.org/packages/11/39/938b111b5bb85a2b07502d0f9d8a704fc75bd760d62e76bce23c89ed16c9/time_machine-3.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9faca6a0f1973d7df3233c951fc2a11ff0c54df74087d8aaf41ae3deb19d0893", size = 34905, upload-time = "2025-12-17T23:31:36.543Z" }, + { url = "https://files.pythonhosted.org/packages/dd/50/0951f73b23e76455de0b4a3a58ac5a24bd8d10489624b1c5e03f10c6fc0b/time_machine-3.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:213b1ada7f385d467e598999b642eda4a8e89ae10ad5dc4f5d8f672cbf604261", size = 33012, upload-time = "2025-12-17T23:31:37.967Z" }, + { url = "https://files.pythonhosted.org/packages/4f/95/5304912d3dcecc4e14ed222dbe0396352efdf8497534abc3c9edd67a7528/time_machine-3.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:160b6afd94c39855af04d39c58e4cf602406abd6d79427ab80e830ea71789cfb", size = 34104, upload-time = "2025-12-17T23:31:39.449Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/af56518652ec7adac4ced193b7a42c4ff354fef28a412b3b5ffa5763aead/time_machine-3.2.0-cp310-cp310-win32.whl", hash = "sha256:c15d9ac257c78c124d112e4fc91fa9f3dcb004bdda913c19f0e7368d713cf080", size = 17468, upload-time = "2025-12-17T23:31:40.432Z" }, + { url = "https://files.pythonhosted.org/packages/48/15/0213f00ca3cf6fe1c9fdbd7fd467e801052fc85534f30c0e4684bd474190/time_machine-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:3bf0f428487f93b8fe9d27aa01eccc817885da3290b467341b4a4a795e1d1891", size = 18313, upload-time = "2025-12-17T23:31:41.617Z" }, + { url = "https://files.pythonhosted.org/packages/77/e4/811f96aa7a634b2b264d9a476f3400e710744dda503b4ad87a5c76db32c9/time_machine-3.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:347f6be2129fcd35b1c94b9387fcb2cbe7949b1e649228c5f22949a811b78976", size = 17037, upload-time = "2025-12-17T23:31:42.924Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e1/03aae5fbaa53859f665094af696338fc7cae733d926a024af69982712350/time_machine-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c188a9dda9fcf975022f1b325b466651b96a4dfc223c523ed7ed8d979f9bf3e8", size = 19143, upload-time = "2025-12-17T23:31:44.258Z" }, + { url = "https://files.pythonhosted.org/packages/75/8f/98cb17bebb52b22ff4ec26984dd44280f9c71353c3bae0640a470e6683e5/time_machine-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17245f1cc2dd13f9d63a174be59bb2684a9e5e0a112ab707e37be92068cd655f", size = 15273, upload-time = "2025-12-17T23:31:45.246Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2f/ca11e4a7897234bb9331fcc5f4ed4714481ba4012370cc79a0ae8c42ea0a/time_machine-3.2.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d9bd1de1996e76efd36ae15970206c5089fb3728356794455bd5cd8d392b5537", size = 31049, upload-time = "2025-12-17T23:31:46.613Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ad/d17d83a59943094e6b6c6a3743caaf6811b12203c3e07a30cc7bcc2ab7ee/time_machine-3.2.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:98493cd50e8b7f941eab69b9e18e697ad69db1a0ec1959f78f3d7b0387107e5c", size = 32632, upload-time = "2025-12-17T23:31:47.72Z" }, + { url = "https://files.pythonhosted.org/packages/71/50/d60576d047a0dfb5638cdfb335e9c3deb6e8528544fa0b3966a8480f72b7/time_machine-3.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:31f2a33d595d9f91eb9bc7f157f0dc5721f5789f4c4a9e8b852cdedb2a7d9b16", size = 34289, upload-time = "2025-12-17T23:31:48.913Z" }, + { url = "https://files.pythonhosted.org/packages/fa/fe/4afa602dbdebddde6d0ea4a7fe849e49b9bb85dc3fb415725a87ccb4b471/time_machine-3.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9f78ac4213c10fbc44283edd1a29cfb7d3382484f4361783ddc057292aaa1889", size = 33175, upload-time = "2025-12-17T23:31:50.611Z" }, + { url = "https://files.pythonhosted.org/packages/0d/87/c152e23977c1d7d7c94eb3ed3ea45cc55971796205125c6fdff40db2c60f/time_machine-3.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c1326b09e947b360926d529a96d1d9e126ce120359b63b506ecdc6ee20755c23", size = 31170, upload-time = "2025-12-17T23:31:51.645Z" }, + { url = "https://files.pythonhosted.org/packages/80/af/54acf51d0f3ade3b51eab73df6192937c9a938753ef5456dff65eb8630be/time_machine-3.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9f2949f03d15264cc15c38918a2cda8966001f0f4ebe190cbfd9c56d91aed8ac", size = 32292, upload-time = "2025-12-17T23:31:52.803Z" }, + { url = "https://files.pythonhosted.org/packages/cc/bc/3745963f36e75661a807196428639327a366f4332f35f1f775c074d4062f/time_machine-3.2.0-cp311-cp311-win32.whl", hash = "sha256:6dfe48e0499e6e16751476b9799e67be7514e6ef04cdf39571ef95a279645831", size = 17349, upload-time = "2025-12-17T23:31:54.19Z" }, + { url = "https://files.pythonhosted.org/packages/82/a2/057469232a99d1f5a0160ae7c5bae7b095c9168b333dd598fcbcfbc1c87b/time_machine-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:809bdf267a29189c304154873620fe0bcc0c9513295fa46b19e21658231c4915", size = 18191, upload-time = "2025-12-17T23:31:55.472Z" }, + { url = "https://files.pythonhosted.org/packages/79/d8/bf9c8de57262ee7130d92a6ed49ed6a6e40a36317e46979428d373630c12/time_machine-3.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:a3f4c17fa90f54902a3f8692c75caf67be87edc3429eeb71cb4595da58198f8e", size = 16905, upload-time = "2025-12-17T23:31:56.658Z" }, + { url = "https://files.pythonhosted.org/packages/71/8b/080c8eedcd67921a52ba5bd0e075362062509ab63c86fc1a0442fad241a6/time_machine-3.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cc4bee5b0214d7dc4ebc91f4a4c600f1a598e9b5606ac751f42cb6f6740b1dbb", size = 19255, upload-time = "2025-12-17T23:31:58.057Z" }, + { url = "https://files.pythonhosted.org/packages/66/17/0e5291e9eb705bf8a5a1305f826e979af307bbeb79def4ddbf4b3f9a81e0/time_machine-3.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ca036304b4460ae2fdc1b52dd8b1fa7cf1464daa427fc49567413c09aa839c1", size = 15360, upload-time = "2025-12-17T23:31:59.048Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/9ab87b71d2e2b62463b9b058b7ae7ac09fb57f8fcd88729dec169d304340/time_machine-3.2.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5442735b41d7a2abc2f04579b4ca6047ed4698a8338a4fec92c7c9423e7938cb", size = 33029, upload-time = "2025-12-17T23:32:00.413Z" }, + { url = "https://files.pythonhosted.org/packages/4b/26/b5ca19da6f25ea905b3e10a0ea95d697c1aeba0404803a43c68f1af253e6/time_machine-3.2.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:97da3e971e505cb637079fb07ab0bcd36e33279f8ecac888ff131f45ef1e4d8d", size = 34579, upload-time = "2025-12-17T23:32:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/79/ca/6ac7ad5f10ea18cc1d9de49716ba38c32132c7b64532430d92ef240c116b/time_machine-3.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3cdda6dee4966e38aeb487309bb414c6cb23a81fc500291c77a8fcd3098832e7", size = 35961, upload-time = "2025-12-17T23:32:02.521Z" }, + { url = "https://files.pythonhosted.org/packages/33/67/390dd958bed395ab32d79a9fe61fe111825c0dd4ded54dbba7e867f171e6/time_machine-3.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:33d9efd302a6998bcc8baa4d84f259f8a4081105bd3d7f7af7f1d0abd3b1c8aa", size = 34668, upload-time = "2025-12-17T23:32:03.585Z" }, + { url = "https://files.pythonhosted.org/packages/da/57/c88fff034a4e9538b3ae7c68c9cfb283670b14d17522c5a8bc17d29f9a4b/time_machine-3.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3a0b0a33971f14145853c9bd95a6ab0353cf7e0019fa2a7aa1ae9fddfe8eab50", size = 32891, upload-time = "2025-12-17T23:32:04.656Z" }, + { url = "https://files.pythonhosted.org/packages/2d/70/ebbb76022dba0fec8f9156540fc647e4beae1680c787c01b1b6200e56d70/time_machine-3.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2d0be9e5f22c38082d247a2cdcd8a936504e9db60b7b3606855fb39f299e9548", size = 34080, upload-time = "2025-12-17T23:32:06.146Z" }, + { url = "https://files.pythonhosted.org/packages/db/9a/2ca9e7af3df540dc1c79e3de588adeddb7dcc2107829248e6969c4f14167/time_machine-3.2.0-cp312-cp312-win32.whl", hash = "sha256:3f74623648b936fdce5f911caf386c0a0b579456410975de8c0dfeaaffece1d8", size = 17371, upload-time = "2025-12-17T23:32:07.164Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ce/21d23efc9c2151939af1b7ee4e60d86d661b74ef32b8eaa148f6fe8c899c/time_machine-3.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:34e26a41d994b5e4b205136a90e9578470386749cc9a2ecf51ca18f83ce25e23", size = 18132, upload-time = "2025-12-17T23:32:08.447Z" }, + { url = "https://files.pythonhosted.org/packages/2f/34/c2b70be483accf6db9e5d6c3139bce3c38fe51f898ccf64e8d3fe14fbf4d/time_machine-3.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:0615d3d82c418d6293f271c348945c5091a71f37e37173653d5c26d0e74b13a8", size = 16930, upload-time = "2025-12-17T23:32:09.477Z" }, + { url = "https://files.pythonhosted.org/packages/ee/cd/43ad5efc88298af3c59b66769cea7f055567a85071579ed40536188530c1/time_machine-3.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c421a8eb85a4418a7675a41bf8660224318c46cc62e4751c8f1ceca752059090", size = 19318, upload-time = "2025-12-17T23:32:10.518Z" }, + { url = "https://files.pythonhosted.org/packages/b0/f6/084010ef7f4a3f38b5a4900923d7c85b29e797655c4f6ee4ce54d903cca8/time_machine-3.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f4e758f7727d0058c4950c66b58200c187072122d6f7a98b610530a4233ea7b", size = 15390, upload-time = "2025-12-17T23:32:11.625Z" }, + { url = "https://files.pythonhosted.org/packages/25/aa/1cabb74134f492270dc6860cb7865859bf40ecf828be65972827646e91ad/time_machine-3.2.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:154bd3f75c81f70218b2585cc12b60762fb2665c507eec5ec5037d8756d9b4e0", size = 33115, upload-time = "2025-12-17T23:32:13.219Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/78c5d7dfa366924eb4dbfcc3fc917c39a4280ca234b12819cc1f16c03d88/time_machine-3.2.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d50cfe5ebea422c896ad8d278af9648412b7533b8ea6adeeee698a3fd9b1d3b7", size = 34705, upload-time = "2025-12-17T23:32:14.29Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/d5e877c24541f674c6869ff6e9c56833369796010190252e92c9d7ae5f0f/time_machine-3.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:636576501724bd6a9124e69d86e5aef263479e89ef739c5db361469f0463a0a1", size = 36104, upload-time = "2025-12-17T23:32:15.354Z" }, + { url = "https://files.pythonhosted.org/packages/22/1c/d4bae72f388f67efc9609f89b012e434bb19d9549c7a7b47d6c7d9e5c55d/time_machine-3.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40e6f40c57197fcf7ec32d2c563f4df0a82c42cdcc3cab27f688e98f6060df10", size = 34765, upload-time = "2025-12-17T23:32:16.434Z" }, + { url = "https://files.pythonhosted.org/packages/1d/c3/ac378cf301d527d8dfad2f0db6bad0dfb1ab73212eaa56d6b96ee5d9d20b/time_machine-3.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a1bcf0b846bbfc19a79bc19e3fa04d8c7b1e8101c1b70340ffdb689cd801ea53", size = 33010, upload-time = "2025-12-17T23:32:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/06/35/7ce897319accda7a6970b288a9a8c52d25227342a7508505a2b3d235b649/time_machine-3.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ae55a56c179f4fe7a62575ad5148b6ed82f6c7e5cf2f9a9ec65f2f5b067db5f5", size = 34185, upload-time = "2025-12-17T23:32:18.566Z" }, + { url = "https://files.pythonhosted.org/packages/bf/28/f922022269749cb02eee2b62919671153c4088994fa955a6b0e50327ff81/time_machine-3.2.0-cp313-cp313-win32.whl", hash = "sha256:a66fe55a107e46916007a391d4030479df8864ec6ad6f6a6528221befc5c886e", size = 17397, upload-time = "2025-12-17T23:32:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/fd87cde397f4a7bea493152f0aca8fd569ec709cad9e0f2ca7011eb8c7f7/time_machine-3.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:30c9ce57165df913e4f74e285a8ab829ff9b7aa3e5ec0973f88f642b9a7b3d15", size = 18139, upload-time = "2025-12-17T23:32:20.991Z" }, + { url = "https://files.pythonhosted.org/packages/75/81/b8ce58233addc5d7d54d2fabc49dcbc02d79e3f079d150aa1bec3d5275ef/time_machine-3.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:89cad7e179e9bdcc84dcf09efe52af232c4cc7a01b3de868356bbd59d95bd9b8", size = 16964, upload-time = "2025-12-17T23:32:22.075Z" }, + { url = "https://files.pythonhosted.org/packages/67/e7/487f0ba5fe6c58186a5e1af2a118dfa2c160fedb37ef53a7e972d410408e/time_machine-3.2.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:59d71545e62525a4b85b6de9ab5c02ee3c61110fd7f636139914a2335dcbfc9c", size = 20000, upload-time = "2025-12-17T23:32:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/e1/17/eb2c0054c8d44dd42df84ccd434539249a9c7d0b8eb53f799be2102500ab/time_machine-3.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:999672c621c35362bc28e03ca0c7df21500195540773c25993421fd8d6cc5003", size = 15657, upload-time = "2025-12-17T23:32:24.125Z" }, + { url = "https://files.pythonhosted.org/packages/43/21/93443b5d1dd850f8bb9442e90d817a9033dcce6bfbdd3aabbb9786251c80/time_machine-3.2.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5faf7397f0580c7b9d67288522c8d7863e85f0cffadc0f1fccdb2c3dfce5783e", size = 39216, upload-time = "2025-12-17T23:32:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/9f/9e/18544cf8acc72bb1dc03762231c82ecc259733f4bb6770a7bbe5cd138603/time_machine-3.2.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3dd886ec49f1fa5a00e844f5947e5c0f98ce574750c24b7424c6f77fc1c3e87", size = 40764, upload-time = "2025-12-17T23:32:26.643Z" }, + { url = "https://files.pythonhosted.org/packages/27/f7/9fe9ce2795636a3a7467307af6bdf38bb613ddb701a8a5cd50ec713beb5e/time_machine-3.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da0ecd96bc7bbe450acaaabe569d84e81688f1be8ad58d1470e42371d145fb53", size = 43526, upload-time = "2025-12-17T23:32:27.693Z" }, + { url = "https://files.pythonhosted.org/packages/03/c1/a93e975ba9dec22e87ec92d18c28e67d36bd536f9119ffa439b2892b0c9c/time_machine-3.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:158220e946c1c4fb8265773a0282c88c35a7e3bb5d78e3561214e3b3231166f3", size = 41727, upload-time = "2025-12-17T23:32:28.985Z" }, + { url = "https://files.pythonhosted.org/packages/5f/fb/e3633e5a6bbed1c76bb2e9810dabc2f8467532ffcd29b9aed404b473061a/time_machine-3.2.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c1aee29bc54356f248d5d7dfdd131e12ca825e850a08c0ebdb022266d073013", size = 38952, upload-time = "2025-12-17T23:32:30.031Z" }, + { url = "https://files.pythonhosted.org/packages/82/3d/02e9fb2526b3d6b1b45bc8e4d912d95d1cd699d1a3f6df985817d37a0600/time_machine-3.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c8ed2224f09d25b1c2fc98683613aca12f90f682a427eabb68fc824d27014e4a", size = 39829, upload-time = "2025-12-17T23:32:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/85/c8/c14265212436da8e0814c45463987b3f57de3eca4de023cc2eabb0c62ef3/time_machine-3.2.0-cp313-cp313t-win32.whl", hash = "sha256:3498719f8dab51da76d29a20c1b5e52ee7db083dddf3056af7fa69c1b94e1fe6", size = 17852, upload-time = "2025-12-17T23:32:32.079Z" }, + { url = "https://files.pythonhosted.org/packages/1d/bc/8acb13cf6149f47508097b158a9a8bec9ec4530a70cb406124e8023581f5/time_machine-3.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e0d90bee170b219e1d15e6a58164aa808f5170090e4f090bd0670303e34181b1", size = 18918, upload-time = "2025-12-17T23:32:33.106Z" }, + { url = "https://files.pythonhosted.org/packages/24/87/c443ee508c2708fd2514ccce9052f5e48888783ce690506919629ebc8eb0/time_machine-3.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:051de220fdb6e20d648111bbad423d9506fdbb2e44d4429cef3dc0382abf1fc2", size = 17261, upload-time = "2025-12-17T23:32:34.446Z" }, + { url = "https://files.pythonhosted.org/packages/61/70/b4b980d126ed155c78d1879c50d60c8dcbd47bd11cb14ee7be50e0dfc07f/time_machine-3.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:1398980c017fe5744d66f419e0115ee48a53b00b146d738e1416c225eb610b82", size = 19303, upload-time = "2025-12-17T23:32:35.796Z" }, + { url = "https://files.pythonhosted.org/packages/73/73/eaa33603c69a68fe2b6f54f9dd75481693d62f1d29676531002be06e2d1c/time_machine-3.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4f8f4e35f4191ef70c2ab8ff490761ee9051b891afce2bf86dde3918eb7b537b", size = 15431, upload-time = "2025-12-17T23:32:37.244Z" }, + { url = "https://files.pythonhosted.org/packages/76/10/b81e138e86cc7bab40cdb59d294b341e172201f4a6c84bb0ec080407977a/time_machine-3.2.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6db498686ecf6163c5aa8cf0bcd57bbe0f4081184f247edf3ee49a2612b584f9", size = 33206, upload-time = "2025-12-17T23:32:38.713Z" }, + { url = "https://files.pythonhosted.org/packages/d3/72/4deab446b579e8bd5dca91de98595c5d6bd6a17ce162abf5c5f2ce40d3d8/time_machine-3.2.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:027c1807efb74d0cd58ad16524dec94212fbe900115d70b0123399883657ac0f", size = 34792, upload-time = "2025-12-17T23:32:40.223Z" }, + { url = "https://files.pythonhosted.org/packages/2c/39/439c6b587ddee76d533fe972289d0646e0a5520e14dc83d0a30aeb5565f7/time_machine-3.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92432610c05676edd5e6946a073c6f0c926923123ce7caee1018dc10782c713d", size = 36187, upload-time = "2025-12-17T23:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/4b/db/2da4368db15180989bab83746a857bde05ad16e78f326801c142bb747a06/time_machine-3.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c25586b62480eb77ef3d953fba273209478e1ef49654592cd6a52a68dfe56a67", size = 34855, upload-time = "2025-12-17T23:32:42.817Z" }, + { url = "https://files.pythonhosted.org/packages/88/84/120a431fee50bc4c241425bee4d3a4910df4923b7ab5f7dff1bf0c772f08/time_machine-3.2.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6bf3a2fa738d15e0b95d14469a0b8ea42635467408d8b490e263d5d45c9a177f", size = 33222, upload-time = "2025-12-17T23:32:43.94Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ea/89cfda82bb8c57ff91bb9a26751aa234d6d90e9b4d5ab0ad9dce0f9f0329/time_machine-3.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ce76b82276d7ad2a66cdc85dad4df19d1422b69183170a34e8fbc4c3f35502f7", size = 34270, upload-time = "2025-12-17T23:32:45.037Z" }, + { url = "https://files.pythonhosted.org/packages/8a/aa/235357da4f69a51a8d35fcbfcfa77cdc7dc24f62ae54025006570bda7e2d/time_machine-3.2.0-cp314-cp314-win32.whl", hash = "sha256:14d6778273c543441863dff712cd1d7803dee946b18de35921eb8df10714539d", size = 17544, upload-time = "2025-12-17T23:32:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/7b/51/6c8405a7276be79693b792cff22ce41067ec05db26a7d02f2d5b06324434/time_machine-3.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbf821da96dbc80d349fa9e7c36e670b41d68a878d28c8850057992fed430eef", size = 18423, upload-time = "2025-12-17T23:32:47.468Z" }, + { url = "https://files.pythonhosted.org/packages/d9/03/a3cf419e20c35fc203c6e4fed48b5b667c1a2b4da456d9971e605f73ecef/time_machine-3.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:71c75d71f8e68abc8b669bca26ed2ddd558430a6c171e32b8620288565f18c0e", size = 17050, upload-time = "2025-12-17T23:32:48.91Z" }, + { url = "https://files.pythonhosted.org/packages/86/a1/142de946dc4393f910bf4564b5c3ba819906e1f49b06c9cb557519c849e4/time_machine-3.2.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4e374779021446fc2b5c29d80457ec9a3b1a5df043dc2aae07d7c1415d52323c", size = 19991, upload-time = "2025-12-17T23:32:49.933Z" }, + { url = "https://files.pythonhosted.org/packages/ee/62/7f17def6289901f94726921811a16b9adce46e666362c75d45730c60274f/time_machine-3.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:122310a6af9c36e9a636da32830e591e7923e8a07bdd0a43276c3a36c6821c90", size = 15707, upload-time = "2025-12-17T23:32:50.969Z" }, + { url = "https://files.pythonhosted.org/packages/5d/d3/3502fb9bd3acb159c18844b26c43220201a0d4a622c0c853785d07699a92/time_machine-3.2.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ba3eeb0f018cc362dd8128befa3426696a2e16dd223c3fb695fde184892d4d8c", size = 39207, upload-time = "2025-12-17T23:32:52.033Z" }, + { url = "https://files.pythonhosted.org/packages/5a/be/8b27f4aa296fda14a5a2ad7f588ddd450603c33415ab3f8e85b2f1a44678/time_machine-3.2.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:77d38ba664b381a7793f8786efc13b5004f0d5f672dae814430445b8202a67a6", size = 40764, upload-time = "2025-12-17T23:32:53.167Z" }, + { url = "https://files.pythonhosted.org/packages/42/cd/fe4c4e5c8ab6d48fab3624c32be9116fb120173a35fe67e482e5cf68b3d2/time_machine-3.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f09abeb8f03f044d72712207e0489a62098ad3ad16dac38927fcf80baca4d6a7", size = 43508, upload-time = "2025-12-17T23:32:54.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/28/5a3ba2fce85b97655a425d6bb20a441550acd2b304c96b2c19d3839f721a/time_machine-3.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6b28367ce4f73987a55e230e1d30a57a3af85da8eb1a140074eb6e8c7e6ef19f", size = 41712, upload-time = "2025-12-17T23:32:55.781Z" }, + { url = "https://files.pythonhosted.org/packages/81/58/e38084be7fdabb4835db68a3a47e58c34182d79fc35df1ecbe0db2c5359f/time_machine-3.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:903c7751c904581da9f7861c3015bed7cdc40047321291d3694a3cdc783bbca3", size = 38939, upload-time = "2025-12-17T23:32:56.867Z" }, + { url = "https://files.pythonhosted.org/packages/40/d0/ad3feb0a392ef4e0c08bc32024950373ddc0669002cbdcbb9f3bf0c2d114/time_machine-3.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:528217cad85ede5f85c8bc78b0341868d3c3cfefc6ecb5b622e1cacb6c73247b", size = 39837, upload-time = "2025-12-17T23:32:58.283Z" }, + { url = "https://files.pythonhosted.org/packages/5b/9e/5f4b2ea63b267bd78f3245e76f5528836611b5f2d30b5e7300a722fe4428/time_machine-3.2.0-cp314-cp314t-win32.whl", hash = "sha256:75724762ffd517e7e80aaec1fad1ff5a7414bd84e2b3ee7a0bacfeb67c14926e", size = 18091, upload-time = "2025-12-17T23:32:59.403Z" }, + { url = "https://files.pythonhosted.org/packages/39/6f/456b1f4d2700ae02b19eba830f870596a4b89b74bac3b6c80666f1b108c5/time_machine-3.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2526abbd053c5bca898d1b3e7898eec34626b12206718d8c7ce88fd12c1c9c5c", size = 19208, upload-time = "2025-12-17T23:33:00.488Z" }, + { url = "https://files.pythonhosted.org/packages/2f/22/8063101427ecd3d2652aada4d21d0876b07a3dc789125bca2ee858fec3ed/time_machine-3.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:7f2fb6784b414edbe2c0b558bfaab0c251955ba27edd62946cce4a01675a992c", size = 17359, upload-time = "2025-12-17T23:33:01.54Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "extra == 'group-18-runloop-api-client-pydantic-v2' or extra != 'group-18-runloop-api-client-pydantic-v1'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "yarl" +version = "1.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/43/a2204825342f37c337f5edb6637040fa14e365b2fcc2346960201d457579/yarl-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e", size = 140517, upload-time = "2025-10-06T14:08:42.494Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/674f3e6f02266428c56f704cd2501c22f78e8b2eeb23f153117cc86fb28a/yarl-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f", size = 93495, upload-time = "2025-10-06T14:08:46.2Z" }, + { url = "https://files.pythonhosted.org/packages/b8/12/5b274d8a0f30c07b91b2f02cba69152600b47830fcfb465c108880fcee9c/yarl-1.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf", size = 94400, upload-time = "2025-10-06T14:08:47.855Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7f/df1b6949b1fa1aa9ff6de6e2631876ad4b73c4437822026e85d8acb56bb1/yarl-1.22.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a", size = 347545, upload-time = "2025-10-06T14:08:49.683Z" }, + { url = "https://files.pythonhosted.org/packages/84/09/f92ed93bd6cd77872ab6c3462df45ca45cd058d8f1d0c9b4f54c1704429f/yarl-1.22.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c", size = 319598, upload-time = "2025-10-06T14:08:51.215Z" }, + { url = "https://files.pythonhosted.org/packages/c3/97/ac3f3feae7d522cf7ccec3d340bb0b2b61c56cb9767923df62a135092c6b/yarl-1.22.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147", size = 363893, upload-time = "2025-10-06T14:08:53.144Z" }, + { url = "https://files.pythonhosted.org/packages/06/49/f3219097403b9c84a4d079b1d7bda62dd9b86d0d6e4428c02d46ab2c77fc/yarl-1.22.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb", size = 371240, upload-time = "2025-10-06T14:08:55.036Z" }, + { url = "https://files.pythonhosted.org/packages/35/9f/06b765d45c0e44e8ecf0fe15c9eacbbde342bb5b7561c46944f107bfb6c3/yarl-1.22.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6", size = 346965, upload-time = "2025-10-06T14:08:56.722Z" }, + { url = "https://files.pythonhosted.org/packages/c5/69/599e7cea8d0fcb1694323b0db0dda317fa3162f7b90166faddecf532166f/yarl-1.22.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0", size = 342026, upload-time = "2025-10-06T14:08:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/95/6f/9dfd12c8bc90fea9eab39832ee32ea48f8e53d1256252a77b710c065c89f/yarl-1.22.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda", size = 335637, upload-time = "2025-10-06T14:09:00.506Z" }, + { url = "https://files.pythonhosted.org/packages/57/2e/34c5b4eb9b07e16e873db5b182c71e5f06f9b5af388cdaa97736d79dd9a6/yarl-1.22.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc", size = 359082, upload-time = "2025-10-06T14:09:01.936Z" }, + { url = "https://files.pythonhosted.org/packages/31/71/fa7e10fb772d273aa1f096ecb8ab8594117822f683bab7d2c5a89914c92a/yarl-1.22.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737", size = 357811, upload-time = "2025-10-06T14:09:03.445Z" }, + { url = "https://files.pythonhosted.org/packages/26/da/11374c04e8e1184a6a03cf9c8f5688d3e5cec83ed6f31ad3481b3207f709/yarl-1.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467", size = 351223, upload-time = "2025-10-06T14:09:05.401Z" }, + { url = "https://files.pythonhosted.org/packages/82/8f/e2d01f161b0c034a30410e375e191a5d27608c1f8693bab1a08b089ca096/yarl-1.22.0-cp310-cp310-win32.whl", hash = "sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea", size = 82118, upload-time = "2025-10-06T14:09:11.148Z" }, + { url = "https://files.pythonhosted.org/packages/62/46/94c76196642dbeae634c7a61ba3da88cd77bed875bf6e4a8bed037505aa6/yarl-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca", size = 86852, upload-time = "2025-10-06T14:09:12.958Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/7df4f179d3b1a6dcb9a4bd2ffbc67642746fcafdb62580e66876ce83fff4/yarl-1.22.0-cp310-cp310-win_arm64.whl", hash = "sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b", size = 82012, upload-time = "2025-10-06T14:09:14.664Z" }, + { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" }, + { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" }, + { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" }, + { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" }, + { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" }, + { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" }, + { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" }, + { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" }, + { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" }, + { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" }, + { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" }, + { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" }, + { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" }, + { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" }, + { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" }, + { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" }, + { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" }, + { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" }, + { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" }, + { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" }, + { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, + { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" }, + { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" }, + { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" }, + { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" }, + { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" }, + { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" }, + { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" }, + { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" }, + { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" }, + { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" }, + { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" }, + { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" }, + { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" }, + { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, + { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" }, + { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" }, + { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" }, + { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" }, + { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" }, + { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" }, + { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" }, + { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" }, + { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" }, + { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" }, + { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" }, + { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" }, + { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" }, + { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, + { url = "https://files.pythonhosted.org/packages/94/fd/6480106702a79bcceda5fd9c63cb19a04a6506bd5ce7fd8d9b63742f0021/yarl-1.22.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3aa27acb6de7a23785d81557577491f6c38a5209a254d1191519d07d8fe51748", size = 141301, upload-time = "2025-10-06T14:12:19.01Z" }, + { url = "https://files.pythonhosted.org/packages/42/e1/6d95d21b17a93e793e4ec420a925fe1f6a9342338ca7a563ed21129c0990/yarl-1.22.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:af74f05666a5e531289cb1cc9c883d1de2088b8e5b4de48004e5ca8a830ac859", size = 93864, upload-time = "2025-10-06T14:12:21.05Z" }, + { url = "https://files.pythonhosted.org/packages/32/58/b8055273c203968e89808413ea4c984988b6649baabf10f4522e67c22d2f/yarl-1.22.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:62441e55958977b8167b2709c164c91a6363e25da322d87ae6dd9c6019ceecf9", size = 94706, upload-time = "2025-10-06T14:12:23.287Z" }, + { url = "https://files.pythonhosted.org/packages/18/91/d7bfbc28a88c2895ecd0da6a874def0c147de78afc52c773c28e1aa233a3/yarl-1.22.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b580e71cac3f8113d3135888770903eaf2f507e9421e5697d6ee6d8cd1c7f054", size = 347100, upload-time = "2025-10-06T14:12:28.527Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e8/37a1e7b99721c0564b1fc7b0a4d1f595ef6fb8060d82ca61775b644185f7/yarl-1.22.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e81fda2fb4a07eda1a2252b216aa0df23ebcd4d584894e9612e80999a78fd95b", size = 318902, upload-time = "2025-10-06T14:12:30.528Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ef/34724449d7ef2db4f22df644f2dac0b8a275d20f585e526937b3ae47b02d/yarl-1.22.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:99b6fc1d55782461b78221e95fc357b47ad98b041e8e20f47c1411d0aacddc60", size = 363302, upload-time = "2025-10-06T14:12:32.295Z" }, + { url = "https://files.pythonhosted.org/packages/8a/04/88a39a5dad39889f192cce8d66cc4c58dbeca983e83f9b6bf23822a7ed91/yarl-1.22.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:088e4e08f033db4be2ccd1f34cf29fe994772fb54cfe004bbf54db320af56890", size = 370816, upload-time = "2025-10-06T14:12:34.01Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1f/5e895e547129413f56c76be2c3ce4b96c797d2d0ff3e16a817d9269b12e6/yarl-1.22.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4e1f6f0b4da23e61188676e3ed027ef0baa833a2e633c29ff8530800edccba", size = 346465, upload-time = "2025-10-06T14:12:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/11/13/a750e9fd6f9cc9ed3a52a70fe58ffe505322f0efe0d48e1fd9ffe53281f5/yarl-1.22.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:84fc3ec96fce86ce5aa305eb4aa9358279d1aa644b71fab7b8ed33fe3ba1a7ca", size = 341506, upload-time = "2025-10-06T14:12:37.788Z" }, + { url = "https://files.pythonhosted.org/packages/3c/67/bb6024de76e7186611ebe626aec5b71a2d2ecf9453e795f2dbd80614784c/yarl-1.22.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5dbeefd6ca588b33576a01b0ad58aa934bc1b41ef89dee505bf2932b22ddffba", size = 335030, upload-time = "2025-10-06T14:12:39.775Z" }, + { url = "https://files.pythonhosted.org/packages/a2/be/50b38447fd94a7992996a62b8b463d0579323fcfc08c61bdba949eef8a5d/yarl-1.22.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14291620375b1060613f4aab9ebf21850058b6b1b438f386cc814813d901c60b", size = 358560, upload-time = "2025-10-06T14:12:41.547Z" }, + { url = "https://files.pythonhosted.org/packages/e2/89/c020b6f547578c4e3dbb6335bf918f26e2f34ad0d1e515d72fd33ac0c635/yarl-1.22.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:a4fcfc8eb2c34148c118dfa02e6427ca278bfd0f3df7c5f99e33d2c0e81eae3e", size = 357290, upload-time = "2025-10-06T14:12:43.861Z" }, + { url = "https://files.pythonhosted.org/packages/8c/52/c49a619ee35a402fa3a7019a4fa8d26878fec0d1243f6968bbf516789578/yarl-1.22.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:029866bde8d7b0878b9c160e72305bbf0a7342bcd20b9999381704ae03308dc8", size = 350700, upload-time = "2025-10-06T14:12:46.868Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c9/f5042d87777bf6968435f04a2bbb15466b2f142e6e47fa4f34d1a3f32f0c/yarl-1.22.0-cp39-cp39-win32.whl", hash = "sha256:4dcc74149ccc8bba31ce1944acee24813e93cfdee2acda3c172df844948ddf7b", size = 82323, upload-time = "2025-10-06T14:12:48.633Z" }, + { url = "https://files.pythonhosted.org/packages/fd/58/d00f7cad9eba20c4eefac2682f34661d1d1b3a942fc0092eb60e78cfb733/yarl-1.22.0-cp39-cp39-win_amd64.whl", hash = "sha256:10619d9fdee46d20edc49d3479e2f8269d0779f1b031e6f7c2aa1c76be04b7ed", size = 87145, upload-time = "2025-10-06T14:12:50.241Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a3/70904f365080780d38b919edd42d224b8c4ce224a86950d2eaa2a24366ad/yarl-1.22.0-cp39-cp39-win_arm64.whl", hash = "sha256:dd7afd3f8b0bfb4e0d9fc3c31bfe8a4ec7debe124cfd90619305def3c8ca8cd2", size = 82173, upload-time = "2025-10-06T14:12:51.869Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +]