diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1243b2cf..b9d5475c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/benchify-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | @@ -44,7 +44,7 @@ jobs: id-token: write runs-on: ${{ github.repository == 'stainless-sdks/benchify-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | @@ -63,7 +63,7 @@ jobs: - name: Get GitHub OIDC Token if: github.repository == 'stainless-sdks/benchify-python' id: github-oidc - uses: actions/github-script@v6 + uses: actions/github-script@v8 with: script: core.setOutput('github_token', await core.getIDToken()); @@ -81,7 +81,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/benchify-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index b9352711..0413e5b0 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index 38c59456..4e8c2627 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'Benchify/benchify-sdk-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Check release environment run: | diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 1b77f506..6538ca91 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.7.0" + ".": "0.8.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index e25c529a..aa71e355 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/benchify%2Fbenchify-0501dfa34f8ffcd73df00592617b82f10843f0b728c90711e16b7271274e0316.yml -openapi_spec_hash: 103dac0edba909886461d4593568fa70 -config_hash: b7ed1548b06567db243d470ec47c9181 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/benchify%2Fbenchify-d345f287418c7ded5f2000038e557efd0aa5acf4a0240e7885c6bc1b842b19fb.yml +openapi_spec_hash: 00a5dedd2dce00eace3d9f33b8ee4282 +config_hash: 4537489a34488f32b7fd5d7987078778 diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c7574ed..3491c8db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,65 @@ # Changelog +## 0.8.0 (2026-02-25) + +Full Changelog: [v0.7.0...v0.8.0](https://github.com/Benchify/benchify-sdk-python/compare/v0.7.0...v0.8.0) + +### Features + +* **api:** api update ([f17bb88](https://github.com/Benchify/benchify-sdk-python/commit/f17bb881929a54d56e181167942b29966e6239e1)) +* **api:** api update ([988433a](https://github.com/Benchify/benchify-sdk-python/commit/988433adb17c6723785fcc79d112d1235864858e)) +* **api:** api update ([acbdbdd](https://github.com/Benchify/benchify-sdk-python/commit/acbdbddbfc8a6bf0c32339b4a68635500cf7bb73)) +* **api:** api update ([3a56790](https://github.com/Benchify/benchify-sdk-python/commit/3a56790797c67b357047e2cf3ffcc857490e28ea)) +* **api:** api update ([b42ea01](https://github.com/Benchify/benchify-sdk-python/commit/b42ea01495e0b0b68712d3845933a1fd16964e1c)) +* **api:** api update ([db0f873](https://github.com/Benchify/benchify-sdk-python/commit/db0f87365147f7478889d69bf6e14807a878c30a)) +* **api:** manual updates ([c3106c7](https://github.com/Benchify/benchify-sdk-python/commit/c3106c7a605a7dc8711ca81f96863a5e20ae81be)) +* **api:** manual updates ([4e28d49](https://github.com/Benchify/benchify-sdk-python/commit/4e28d49d08c13027bf89b728d8139bf6f8a8e1d4)) +* **api:** manual updates ([9d3bc91](https://github.com/Benchify/benchify-sdk-python/commit/9d3bc91b39de22ee755e7afb125665ac7d62d7c7)) +* **api:** manual updates ([86d3bcd](https://github.com/Benchify/benchify-sdk-python/commit/86d3bcd4b43249e2a7e86723184968be5ac435ae)) +* **api:** manual updates ([676a40a](https://github.com/Benchify/benchify-sdk-python/commit/676a40a70134370ecdca261c9804bbe8939e8d9b)) +* **client:** add custom JSON encoder for extended type support ([11383dd](https://github.com/Benchify/benchify-sdk-python/commit/11383dd4f5806a594f3131f423c4e27c150e3200)) +* **client:** add support for binary request streaming ([2c35482](https://github.com/Benchify/benchify-sdk-python/commit/2c35482f9d7fc168d95dc23b7eff85503739b369)) + + +### Bug Fixes + +* **client:** loosen auth header validation ([8c22a38](https://github.com/Benchify/benchify-sdk-python/commit/8c22a38c0969909e0aa24b5a856b59a6f506ba9b)) +* compat with Python 3.14 ([75b948d](https://github.com/Benchify/benchify-sdk-python/commit/75b948d1aa8d9bc26eef28b9a71e39ba8b9b2c9a)) +* **compat:** update signatures of `model_dump` and `model_dump_json` for Pydantic v1 ([096f67b](https://github.com/Benchify/benchify-sdk-python/commit/096f67bad9d1d7f8e55987ec4a1a15e3a41f224a)) +* **docs:** fix mcp installation instructions for remote servers ([378d782](https://github.com/Benchify/benchify-sdk-python/commit/378d7822f3073a570ddbf64f4a4521a210f0fdf9)) +* ensure streams are always closed ([04ce4dc](https://github.com/Benchify/benchify-sdk-python/commit/04ce4dc454aab185e59aec577c12eb89252045f7)) +* **types:** allow pyright to infer TypedDict types within SequenceNotStr ([fae18b6](https://github.com/Benchify/benchify-sdk-python/commit/fae18b6333d3697845bbf233ea20f55c96202561)) +* use async_to_httpx_files in patch method ([7732f70](https://github.com/Benchify/benchify-sdk-python/commit/7732f705f48dd431880c8ed65708a4cf5b8eff06)) + + +### Chores + +* add missing docstrings ([2564e8d](https://github.com/Benchify/benchify-sdk-python/commit/2564e8dbd691cd3ca3d47ed56dcdb21c96ef249f)) +* add Python 3.14 classifier and testing ([dc5218c](https://github.com/Benchify/benchify-sdk-python/commit/dc5218cfd1712d7b2bf5671b2985ae12eae6d4b7)) +* **ci:** upgrade `actions/github-script` ([b0d0279](https://github.com/Benchify/benchify-sdk-python/commit/b0d0279c1f1b22a6ff09802fb1d04dd66dd1c0a4)) +* **deps:** mypy 1.18.1 has a regression, pin to 1.17 ([69a5933](https://github.com/Benchify/benchify-sdk-python/commit/69a5933b2a0671dd5c4d8a50bfa15e0af49d0a9c)) +* **docs:** use environment variables for authentication in code snippets ([0a45222](https://github.com/Benchify/benchify-sdk-python/commit/0a45222f9cbf46b0319c20b382c4d6bf593c148b)) +* format all `api.md` files ([711513d](https://github.com/Benchify/benchify-sdk-python/commit/711513df3f25b7a222c6d6a98843c5a6d9fc1e9c)) +* **internal:** add `--fix` argument to lint script ([3cfbbf7](https://github.com/Benchify/benchify-sdk-python/commit/3cfbbf79912010c381c10a4ec25c0b96eac0a5f9)) +* **internal:** add missing files argument to base client ([d16ba69](https://github.com/Benchify/benchify-sdk-python/commit/d16ba6962962dd2feb5c5d5d9ef6d102f9f6c576)) +* **internal:** add request options to SSE classes ([354018a](https://github.com/Benchify/benchify-sdk-python/commit/354018a24317362a1b089ecfc05cb054bf50fe98)) +* **internal:** bump dependencies ([6681cdf](https://github.com/Benchify/benchify-sdk-python/commit/6681cdf5dde39dcadfd2006194277931dc785d0e)) +* **internal:** codegen related update ([9c77b1b](https://github.com/Benchify/benchify-sdk-python/commit/9c77b1bc44be11429ef47d60004997ac38ec1daa)) +* **internal:** fix lint error on Python 3.14 ([4f8e8e8](https://github.com/Benchify/benchify-sdk-python/commit/4f8e8e8735c4e156faa4d9dba479ca5ad42bf92a)) +* **internal:** make `test_proxy_environment_variables` more resilient ([46524de](https://github.com/Benchify/benchify-sdk-python/commit/46524de6bbe962ba385dd851f14bad6724a727f0)) +* **internal:** make `test_proxy_environment_variables` more resilient to env ([3e63088](https://github.com/Benchify/benchify-sdk-python/commit/3e630889266a7141a7c55608e9a82a0a9ba356da)) +* **internal:** remove mock server code ([10a1f64](https://github.com/Benchify/benchify-sdk-python/commit/10a1f64f9e861aba06f334c933e9ef716b1ea141)) +* **internal:** update `actions/checkout` version ([1450fbd](https://github.com/Benchify/benchify-sdk-python/commit/1450fbd5b449481749a518d3b8c1410ba8473b05)) +* **package:** drop Python 3.8 support ([f5b1d6b](https://github.com/Benchify/benchify-sdk-python/commit/f5b1d6b16383e15a84a2d3a974d8fa5380a7a676)) +* speedup initial import ([3791761](https://github.com/Benchify/benchify-sdk-python/commit/37917619e0f6b6506afa8251aaa3cb0760907886)) +* update lockfile ([9a95a83](https://github.com/Benchify/benchify-sdk-python/commit/9a95a832a4bf7a3b456c8919e5b28adab0666f90)) +* update mock server docs ([8c7ae7b](https://github.com/Benchify/benchify-sdk-python/commit/8c7ae7b7ef535ee037dcccd0f074348dd21b7ea1)) + + +### Documentation + +* prominently feature MCP server setup in root SDK readmes ([73251c3](https://github.com/Benchify/benchify-sdk-python/commit/73251c34c556b8010d7c18d8f200930a9e3b1a87)) + ## 0.7.0 (2025-11-07) Full Changelog: [v0.6.0...v0.7.0](https://github.com/Benchify/benchify-sdk-python/compare/v0.6.0...v0.7.0) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ed2e3d1d..1a870b9b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -85,13 +85,6 @@ $ pip install ./path-to-wheel-file.whl ## Running tests -Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests. - -```sh -# you will need npm installed -$ npx prism mock path/to/your/openapi.yml -``` - ```sh $ ./scripts/test ``` diff --git a/LICENSE b/LICENSE index ca74ba36..f0958add 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2025 Benchify + Copyright 2026 Benchify Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index ead0fef9..d64cc7b5 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,21 @@ [![PyPI version](https://img.shields.io/pypi/v/benchify.svg?label=pypi%20(stable))](https://pypi.org/project/benchify/) -The Benchify Python library provides convenient access to the Benchify REST API from any Python 3.8+ +The Benchify Python library provides convenient access to the Benchify 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 Benchify 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=benchify-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsImJlbmNoaWZ5LW1jcCJdLCJlbnYiOnsiQkVOQ0hJRllfQVBJX0tFWSI6Ik15IEFQSSBLZXkifX0) +[![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%22benchify-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22benchify-mcp%22%5D%2C%22env%22%3A%7B%22BENCHIFY_API_KEY%22%3A%22My%20API%20Key%22%7D%7D) + +> Note: You may need to set environment variables in your MCP client. + ## Documentation The REST API documentation can be found on [benchify.com](https://benchify.com/support). The full API of this library can be found in [api.md](api.md). @@ -79,6 +88,7 @@ pip install benchify[aiohttp] Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: ```python +import os import asyncio from benchify import DefaultAioHttpClient from benchify import AsyncBenchify @@ -86,7 +96,7 @@ from benchify import AsyncBenchify async def main() -> None: async with AsyncBenchify( - api_key="My API Key", + api_key=os.environ.get("BENCHIFY_API_KEY"), # This is the default and can be omitted http_client=DefaultAioHttpClient(), ) as client: response = await client.fixer.run() @@ -390,7 +400,7 @@ print(benchify.__version__) ## Requirements -Python 3.8 or higher. +Python 3.9 or higher. ## Contributing diff --git a/api.md b/api.md index b8c46340..7ebb7d0a 100644 --- a/api.md +++ b/api.md @@ -19,7 +19,7 @@ from benchify.types import ( StackCreateResponse, StackRetrieveResponse, StackUpdateResponse, - StackCreateAndRunResponse, + StackBundleMultipartResponse, StackExecuteCommandResponse, StackGetLogsResponse, StackGetNetworkInfoResponse, @@ -35,7 +35,7 @@ Methods: - client.stacks.create(\*\*params) -> StackCreateResponse - client.stacks.retrieve(id) -> StackRetrieveResponse - client.stacks.update(id, \*\*params) -> StackUpdateResponse -- client.stacks.create_and_run(\*\*params) -> StackCreateAndRunResponse +- client.stacks.bundle_multipart(\*\*params) -> StackBundleMultipartResponse - client.stacks.destroy(id) -> None - client.stacks.execute_command(id, \*\*params) -> StackExecuteCommandResponse - client.stacks.get_logs(id, \*\*params) -> StackGetLogsResponse diff --git a/pyproject.toml b/pyproject.toml index 1b88b18b..3aeb034a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,30 +1,32 @@ [project] name = "benchify" -version = "0.7.0" +version = "0.8.0" description = "The official Python library for the benchify API" dynamic = ["readme"] license = "Apache-2.0" authors = [ { name = "Benchify", email = "support@benchify.com" }, ] + dependencies = [ - "httpx>=0.23.0, <1", - "pydantic>=1.9.0, <3", - "typing-extensions>=4.10, <5", - "anyio>=3.5.0, <5", - "distro>=1.7.0, <2", - "sniffio", + "httpx>=0.23.0, <1", + "pydantic>=1.9.0, <3", + "typing-extensions>=4.10, <5", + "anyio>=3.5.0, <5", + "distro>=1.7.0, <2", + "sniffio", ] -requires-python = ">= 3.8" + +requires-python = ">= 3.9" classifiers = [ "Typing :: Typed", "Intended Audience :: Developers", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "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", @@ -46,7 +48,7 @@ managed = true # version pins are in requirements-dev.lock dev-dependencies = [ "pyright==1.1.399", - "mypy", + "mypy==1.17", "respx", "pytest", "pytest-asyncio", @@ -67,7 +69,7 @@ format = { chain = [ # run formatting again to fix any inconsistencies when imports are stripped "format:ruff", ]} -"format:docs" = "python scripts/utils/ruffen-docs.py README.md api.md" +"format:docs" = "bash -c 'python scripts/utils/ruffen-docs.py README.md $(find . -type f -name api.md)'" "format:ruff" = "ruff format" "lint" = { chain = [ @@ -141,7 +143,7 @@ filterwarnings = [ # there are a couple of flags that are still disabled by # default in strict mode as they are experimental and niche. typeCheckingMode = "strict" -pythonVersion = "3.8" +pythonVersion = "3.9" exclude = [ "_dev", diff --git a/requirements-dev.lock b/requirements-dev.lock index 0e0d6cab..d83234da 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -12,40 +12,45 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.12.8 +aiohttp==3.13.3 # via benchify # via httpx-aiohttp -aiosignal==1.3.2 +aiosignal==1.4.0 # via aiohttp -annotated-types==0.6.0 +annotated-types==0.7.0 # via pydantic -anyio==4.4.0 +anyio==4.12.1 # via benchify # via httpx -argcomplete==3.1.2 +argcomplete==3.6.3 # via nox async-timeout==5.0.1 # via aiohttp -attrs==25.3.0 +attrs==25.4.0 # via aiohttp -certifi==2023.7.22 + # via nox +backports-asyncio-runner==1.2.0 + # via pytest-asyncio +certifi==2026.1.4 # via httpcore # via httpx -colorlog==6.7.0 +colorlog==6.10.1 + # via nox +dependency-groups==1.3.1 # via nox -dirty-equals==0.6.0 -distlib==0.3.7 +dirty-equals==0.11 +distlib==0.4.0 # via virtualenv -distro==1.8.0 +distro==1.9.0 # via benchify -exceptiongroup==1.2.2 +exceptiongroup==1.3.1 # via anyio # via pytest -execnet==2.1.1 +execnet==2.1.2 # via pytest-xdist -filelock==3.12.4 +filelock==3.19.1 # via virtualenv -frozenlist==1.6.2 +frozenlist==1.8.0 # via aiohttp # via aiosignal h11==0.16.0 @@ -56,82 +61,89 @@ httpx==0.28.1 # via benchify # via httpx-aiohttp # via respx -httpx-aiohttp==0.1.9 +httpx-aiohttp==0.1.12 # via benchify -idna==3.4 +humanize==4.13.0 + # via nox +idna==3.11 # via anyio # via httpx # via yarl -importlib-metadata==7.0.0 -iniconfig==2.0.0 +importlib-metadata==8.7.1 +iniconfig==2.1.0 # via pytest markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -multidict==6.4.4 +multidict==6.7.0 # via aiohttp # via yarl -mypy==1.14.1 -mypy-extensions==1.0.0 +mypy==1.17.0 +mypy-extensions==1.1.0 # via mypy -nodeenv==1.8.0 +nodeenv==1.10.0 # via pyright -nox==2023.4.22 -packaging==23.2 +nox==2025.11.12 +packaging==25.0 + # via dependency-groups # via nox # via pytest -platformdirs==3.11.0 +pathspec==1.0.3 + # via mypy +platformdirs==4.4.0 # via virtualenv -pluggy==1.5.0 +pluggy==1.6.0 # via pytest -propcache==0.3.1 +propcache==0.4.1 # via aiohttp # via yarl -pydantic==2.11.9 +pydantic==2.12.5 # via benchify -pydantic-core==2.33.2 +pydantic-core==2.41.5 # via pydantic -pygments==2.18.0 +pygments==2.19.2 + # via pytest # via rich pyright==1.1.399 -pytest==8.3.3 +pytest==8.4.2 # via pytest-asyncio # via pytest-xdist -pytest-asyncio==0.24.0 -pytest-xdist==3.7.0 -python-dateutil==2.8.2 +pytest-asyncio==1.2.0 +pytest-xdist==3.8.0 +python-dateutil==2.9.0.post0 # via time-machine -pytz==2023.3.post1 - # via dirty-equals respx==0.22.0 -rich==13.7.1 -ruff==0.9.4 -setuptools==68.2.2 - # via nodeenv -six==1.16.0 +rich==14.2.0 +ruff==0.14.13 +six==1.17.0 # via python-dateutil -sniffio==1.3.0 - # via anyio +sniffio==1.3.1 # via benchify -time-machine==2.9.0 -tomli==2.0.2 +time-machine==2.19.0 +tomli==2.4.0 + # via dependency-groups # via mypy + # via nox # via pytest -typing-extensions==4.12.2 +typing-extensions==4.15.0 + # via aiosignal # via anyio # via benchify + # via exceptiongroup # via multidict # via mypy # via pydantic # via pydantic-core # via pyright + # via pytest-asyncio # via typing-inspection -typing-inspection==0.4.1 + # via virtualenv +typing-inspection==0.4.2 # via pydantic -virtualenv==20.24.5 +virtualenv==20.36.1 # via nox -yarl==1.20.0 +yarl==1.22.0 # via aiohttp -zipp==3.17.0 +zipp==3.23.0 # via importlib-metadata diff --git a/requirements.lock b/requirements.lock index 5346f489..bcb6d79a 100644 --- a/requirements.lock +++ b/requirements.lock @@ -12,28 +12,28 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.12.8 +aiohttp==3.13.3 # via benchify # via httpx-aiohttp -aiosignal==1.3.2 +aiosignal==1.4.0 # via aiohttp -annotated-types==0.6.0 +annotated-types==0.7.0 # via pydantic -anyio==4.4.0 +anyio==4.12.1 # via benchify # via httpx async-timeout==5.0.1 # via aiohttp -attrs==25.3.0 +attrs==25.4.0 # via aiohttp -certifi==2023.7.22 +certifi==2026.1.4 # via httpcore # via httpx -distro==1.8.0 +distro==1.9.0 # via benchify -exceptiongroup==1.2.2 +exceptiongroup==1.3.1 # via anyio -frozenlist==1.6.2 +frozenlist==1.8.0 # via aiohttp # via aiosignal h11==0.16.0 @@ -43,33 +43,34 @@ httpcore==1.0.9 httpx==0.28.1 # via benchify # via httpx-aiohttp -httpx-aiohttp==0.1.9 +httpx-aiohttp==0.1.12 # via benchify -idna==3.4 +idna==3.11 # via anyio # via httpx # via yarl -multidict==6.4.4 +multidict==6.7.0 # via aiohttp # via yarl -propcache==0.3.1 +propcache==0.4.1 # via aiohttp # via yarl -pydantic==2.11.9 +pydantic==2.12.5 # via benchify -pydantic-core==2.33.2 +pydantic-core==2.41.5 # via pydantic -sniffio==1.3.0 - # via anyio +sniffio==1.3.1 # via benchify -typing-extensions==4.12.2 +typing-extensions==4.15.0 + # via aiosignal # via anyio # via benchify + # via exceptiongroup # via multidict # via pydantic # via pydantic-core # via typing-inspection -typing-inspection==0.4.1 +typing-inspection==0.4.2 # via pydantic -yarl==1.20.0 +yarl==1.22.0 # via aiohttp diff --git a/scripts/lint b/scripts/lint index 43a80cef..7de05a16 100755 --- a/scripts/lint +++ b/scripts/lint @@ -4,8 +4,13 @@ set -e cd "$(dirname "$0")/.." -echo "==> Running lints" -rye run lint +if [ "$1" = "--fix" ]; then + echo "==> Running lints with --fix" + rye run fix:ruff +else + echo "==> Running lints" + rye run lint +fi echo "==> Making sure it imports" rye run python -c 'import benchify' diff --git a/scripts/mock b/scripts/mock deleted file mode 100755 index 0b28f6ea..00000000 --- a/scripts/mock +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash - -set -e - -cd "$(dirname "$0")/.." - -if [[ -n "$1" && "$1" != '--'* ]]; then - URL="$1" - shift -else - URL="$(grep 'openapi_spec_url' .stats.yml | cut -d' ' -f2)" -fi - -# Check if the URL is empty -if [ -z "$URL" ]; then - echo "Error: No OpenAPI spec path/url provided or found in .stats.yml" - exit 1 -fi - -echo "==> Starting mock server with URL ${URL}" - -# Run prism mock on the given spec -if [ "$1" == "--daemon" ]; then - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & - - # Wait for server to come online - echo -n "Waiting for server" - while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do - echo -n "." - sleep 0.1 - done - - if grep -q "✖ fatal" ".prism.log"; then - cat .prism.log - exit 1 - fi - - echo -else - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" -fi diff --git a/scripts/test b/scripts/test index dbeda2d2..39729d09 100755 --- a/scripts/test +++ b/scripts/test @@ -4,53 +4,7 @@ set -e cd "$(dirname "$0")/.." -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -NC='\033[0m' # No Color -function prism_is_running() { - curl --silent "http://localhost:4010" >/dev/null 2>&1 -} - -kill_server_on_port() { - pids=$(lsof -t -i tcp:"$1" || echo "") - if [ "$pids" != "" ]; then - kill "$pids" - echo "Stopped $pids." - fi -} - -function is_overriding_api_base_url() { - [ -n "$TEST_API_BASE_URL" ] -} - -if ! is_overriding_api_base_url && ! prism_is_running ; then - # When we exit this script, make sure to kill the background mock server process - trap 'kill_server_on_port 4010' EXIT - - # Start the dev server - ./scripts/mock --daemon -fi - -if is_overriding_api_base_url ; then - echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}" - echo -elif ! prism_is_running ; then - echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server" - echo -e "running against your OpenAPI spec." - echo - echo -e "To run the server, pass in the path or url of your OpenAPI" - echo -e "spec to the prism command:" - echo - echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}" - echo - - exit 1 -else - echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}" - echo -fi export DEFER_PYDANTIC_BUILD=false diff --git a/src/benchify/_base_client.py b/src/benchify/_base_client.py index b5494481..335954ef 100644 --- a/src/benchify/_base_client.py +++ b/src/benchify/_base_client.py @@ -9,6 +9,7 @@ import inspect import logging import platform +import warnings import email.utils from types import TracebackType from random import random @@ -51,9 +52,11 @@ ResponseT, AnyMapping, PostParser, + BinaryTypes, RequestFiles, HttpxSendArgs, RequestOptions, + AsyncBinaryTypes, HttpxRequestFiles, ModelBuilderProtocol, not_given, @@ -83,6 +86,7 @@ APIConnectionError, APIResponseValidationError, ) +from ._utils._json import openapi_dumps log: logging.Logger = logging.getLogger(__name__) @@ -477,8 +481,19 @@ def _build_request( retries_taken: int = 0, ) -> httpx.Request: if log.isEnabledFor(logging.DEBUG): - log.debug("Request options: %s", model_dump(options, exclude_unset=True)) - + log.debug( + "Request options: %s", + model_dump( + options, + exclude_unset=True, + # Pydantic v1 can't dump every type we support in content, so we exclude it for now. + exclude={ + "content", + } + if PYDANTIC_V1 + else {}, + ), + ) kwargs: dict[str, Any] = {} json_data = options.json_data @@ -532,10 +547,18 @@ def _build_request( is_body_allowed = options.method.lower() != "get" if is_body_allowed: - if isinstance(json_data, bytes): + if options.content is not None and json_data is not None: + raise TypeError("Passing both `content` and `json_data` is not supported") + if options.content is not None and files is not None: + raise TypeError("Passing both `content` and `files` is not supported") + if options.content is not None: + kwargs["content"] = options.content + elif isinstance(json_data, bytes): kwargs["content"] = json_data - else: - kwargs["json"] = json_data if is_given(json_data) else None + 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) @@ -1194,6 +1217,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[False] = False, @@ -1206,6 +1230,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[True], @@ -1219,6 +1244,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: bool, @@ -1231,13 +1257,25 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: bool = False, stream_cls: type[_StreamT] | None = None, ) -> ResponseT | _StreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=to_httpx_files(files), **options + method="post", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) @@ -1247,9 +1285,24 @@ def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, + files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if 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( @@ -1258,11 +1311,23 @@ def put( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=to_httpx_files(files), **options + method="put", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return self.request(cast_to, opts) @@ -1272,9 +1337,19 @@ def delete( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) return self.request(cast_to, opts) def get_api_list( @@ -1714,6 +1789,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[False] = False, @@ -1726,6 +1802,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[True], @@ -1739,6 +1816,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: bool, @@ -1751,13 +1829,25 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: bool = False, stream_cls: type[_AsyncStreamT] | None = None, ) -> ResponseT | _AsyncStreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="post", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) @@ -1767,9 +1857,29 @@ async def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, + files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if 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( @@ -1778,11 +1888,23 @@ async def put( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="put", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts) @@ -1792,9 +1914,19 @@ async def delete( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) return await self.request(cast_to, opts) def get_api_list( diff --git a/src/benchify/_client.py b/src/benchify/_client.py index 9b5e8495..8ba2dff5 100644 --- a/src/benchify/_client.py +++ b/src/benchify/_client.py @@ -3,7 +3,7 @@ from __future__ import annotations import os -from typing import Any, Mapping +from typing import TYPE_CHECKING, Any, Mapping from typing_extensions import Self, override import httpx @@ -21,8 +21,8 @@ not_given, ) from ._utils import is_given, get_async_library +from ._compat import cached_property from ._version import __version__ -from .resources import fixer, stacks, validate_template, fix_string_literals, fix_parsing_and_diagnose from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import APIStatusError from ._base_client import ( @@ -30,7 +30,15 @@ SyncAPIClient, AsyncAPIClient, ) -from .resources.fix import fix + +if TYPE_CHECKING: + from .resources import fix, fixer, stacks, validate_template, fix_string_literals, fix_parsing_and_diagnose + from .resources.fixer import FixerResource, AsyncFixerResource + from .resources.stacks import StacksResource, AsyncStacksResource + from .resources.fix.fix import FixResource, AsyncFixResource + from .resources.validate_template import ValidateTemplateResource, AsyncValidateTemplateResource + from .resources.fix_string_literals import FixStringLiteralsResource, AsyncFixStringLiteralsResource + from .resources.fix_parsing_and_diagnose import FixParsingAndDiagnoseResource, AsyncFixParsingAndDiagnoseResource __all__ = [ "Timeout", @@ -45,15 +53,6 @@ class Benchify(SyncAPIClient): - fixer: fixer.FixerResource - stacks: stacks.StacksResource - fix_string_literals: fix_string_literals.FixStringLiteralsResource - validate_template: validate_template.ValidateTemplateResource - fix_parsing_and_diagnose: fix_parsing_and_diagnose.FixParsingAndDiagnoseResource - fix: fix.FixResource - with_raw_response: BenchifyWithRawResponse - with_streaming_response: BenchifyWithStreamedResponse - # client options api_key: str | None @@ -104,14 +103,49 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.fixer = fixer.FixerResource(self) - self.stacks = stacks.StacksResource(self) - self.fix_string_literals = fix_string_literals.FixStringLiteralsResource(self) - self.validate_template = validate_template.ValidateTemplateResource(self) - self.fix_parsing_and_diagnose = fix_parsing_and_diagnose.FixParsingAndDiagnoseResource(self) - self.fix = fix.FixResource(self) - self.with_raw_response = BenchifyWithRawResponse(self) - self.with_streaming_response = BenchifyWithStreamedResponse(self) + @cached_property + def fixer(self) -> FixerResource: + from .resources.fixer import FixerResource + + return FixerResource(self) + + @cached_property + def stacks(self) -> StacksResource: + from .resources.stacks import StacksResource + + return StacksResource(self) + + @cached_property + def fix_string_literals(self) -> FixStringLiteralsResource: + from .resources.fix_string_literals import FixStringLiteralsResource + + return FixStringLiteralsResource(self) + + @cached_property + def validate_template(self) -> ValidateTemplateResource: + from .resources.validate_template import ValidateTemplateResource + + return ValidateTemplateResource(self) + + @cached_property + def fix_parsing_and_diagnose(self) -> FixParsingAndDiagnoseResource: + from .resources.fix_parsing_and_diagnose import FixParsingAndDiagnoseResource + + return FixParsingAndDiagnoseResource(self) + + @cached_property + def fix(self) -> FixResource: + from .resources.fix import FixResource + + return FixResource(self) + + @cached_property + def with_raw_response(self) -> BenchifyWithRawResponse: + return BenchifyWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> BenchifyWithStreamedResponse: + return BenchifyWithStreamedResponse(self) @property @override @@ -137,9 +171,7 @@ def default_headers(self) -> dict[str, str | Omit]: @override def _validate_headers(self, headers: Headers, custom_headers: Headers) -> None: - if self.api_key and headers.get("Authorization"): - return - if isinstance(custom_headers.get("Authorization"), Omit): + if headers.get("Authorization") or isinstance(custom_headers.get("Authorization"), Omit): return raise TypeError( @@ -232,15 +264,6 @@ def _make_status_error( class AsyncBenchify(AsyncAPIClient): - fixer: fixer.AsyncFixerResource - stacks: stacks.AsyncStacksResource - fix_string_literals: fix_string_literals.AsyncFixStringLiteralsResource - validate_template: validate_template.AsyncValidateTemplateResource - fix_parsing_and_diagnose: fix_parsing_and_diagnose.AsyncFixParsingAndDiagnoseResource - fix: fix.AsyncFixResource - with_raw_response: AsyncBenchifyWithRawResponse - with_streaming_response: AsyncBenchifyWithStreamedResponse - # client options api_key: str | None @@ -291,14 +314,49 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.fixer = fixer.AsyncFixerResource(self) - self.stacks = stacks.AsyncStacksResource(self) - self.fix_string_literals = fix_string_literals.AsyncFixStringLiteralsResource(self) - self.validate_template = validate_template.AsyncValidateTemplateResource(self) - self.fix_parsing_and_diagnose = fix_parsing_and_diagnose.AsyncFixParsingAndDiagnoseResource(self) - self.fix = fix.AsyncFixResource(self) - self.with_raw_response = AsyncBenchifyWithRawResponse(self) - self.with_streaming_response = AsyncBenchifyWithStreamedResponse(self) + @cached_property + def fixer(self) -> AsyncFixerResource: + from .resources.fixer import AsyncFixerResource + + return AsyncFixerResource(self) + + @cached_property + def stacks(self) -> AsyncStacksResource: + from .resources.stacks import AsyncStacksResource + + return AsyncStacksResource(self) + + @cached_property + def fix_string_literals(self) -> AsyncFixStringLiteralsResource: + from .resources.fix_string_literals import AsyncFixStringLiteralsResource + + return AsyncFixStringLiteralsResource(self) + + @cached_property + def validate_template(self) -> AsyncValidateTemplateResource: + from .resources.validate_template import AsyncValidateTemplateResource + + return AsyncValidateTemplateResource(self) + + @cached_property + def fix_parsing_and_diagnose(self) -> AsyncFixParsingAndDiagnoseResource: + from .resources.fix_parsing_and_diagnose import AsyncFixParsingAndDiagnoseResource + + return AsyncFixParsingAndDiagnoseResource(self) + + @cached_property + def fix(self) -> AsyncFixResource: + from .resources.fix import AsyncFixResource + + return AsyncFixResource(self) + + @cached_property + def with_raw_response(self) -> AsyncBenchifyWithRawResponse: + return AsyncBenchifyWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncBenchifyWithStreamedResponse: + return AsyncBenchifyWithStreamedResponse(self) @property @override @@ -324,9 +382,7 @@ def default_headers(self) -> dict[str, str | Omit]: @override def _validate_headers(self, headers: Headers, custom_headers: Headers) -> None: - if self.api_key and headers.get("Authorization"): - return - if isinstance(custom_headers.get("Authorization"), Omit): + if headers.get("Authorization") or isinstance(custom_headers.get("Authorization"), Omit): return raise TypeError( @@ -419,67 +475,177 @@ def _make_status_error( class BenchifyWithRawResponse: + _client: Benchify + def __init__(self, client: Benchify) -> None: - self.fixer = fixer.FixerResourceWithRawResponse(client.fixer) - self.stacks = stacks.StacksResourceWithRawResponse(client.stacks) - self.fix_string_literals = fix_string_literals.FixStringLiteralsResourceWithRawResponse( - client.fix_string_literals - ) - self.validate_template = validate_template.ValidateTemplateResourceWithRawResponse(client.validate_template) - self.fix_parsing_and_diagnose = fix_parsing_and_diagnose.FixParsingAndDiagnoseResourceWithRawResponse( - client.fix_parsing_and_diagnose - ) - self.fix = fix.FixResourceWithRawResponse(client.fix) + self._client = client + + @cached_property + def fixer(self) -> fixer.FixerResourceWithRawResponse: + from .resources.fixer import FixerResourceWithRawResponse + + return FixerResourceWithRawResponse(self._client.fixer) + + @cached_property + def stacks(self) -> stacks.StacksResourceWithRawResponse: + from .resources.stacks import StacksResourceWithRawResponse + + return StacksResourceWithRawResponse(self._client.stacks) + + @cached_property + def fix_string_literals(self) -> fix_string_literals.FixStringLiteralsResourceWithRawResponse: + from .resources.fix_string_literals import FixStringLiteralsResourceWithRawResponse + + return FixStringLiteralsResourceWithRawResponse(self._client.fix_string_literals) + + @cached_property + def validate_template(self) -> validate_template.ValidateTemplateResourceWithRawResponse: + from .resources.validate_template import ValidateTemplateResourceWithRawResponse + + return ValidateTemplateResourceWithRawResponse(self._client.validate_template) + + @cached_property + def fix_parsing_and_diagnose(self) -> fix_parsing_and_diagnose.FixParsingAndDiagnoseResourceWithRawResponse: + from .resources.fix_parsing_and_diagnose import FixParsingAndDiagnoseResourceWithRawResponse + + return FixParsingAndDiagnoseResourceWithRawResponse(self._client.fix_parsing_and_diagnose) + + @cached_property + def fix(self) -> fix.FixResourceWithRawResponse: + from .resources.fix import FixResourceWithRawResponse + + return FixResourceWithRawResponse(self._client.fix) class AsyncBenchifyWithRawResponse: + _client: AsyncBenchify + def __init__(self, client: AsyncBenchify) -> None: - self.fixer = fixer.AsyncFixerResourceWithRawResponse(client.fixer) - self.stacks = stacks.AsyncStacksResourceWithRawResponse(client.stacks) - self.fix_string_literals = fix_string_literals.AsyncFixStringLiteralsResourceWithRawResponse( - client.fix_string_literals - ) - self.validate_template = validate_template.AsyncValidateTemplateResourceWithRawResponse( - client.validate_template - ) - self.fix_parsing_and_diagnose = fix_parsing_and_diagnose.AsyncFixParsingAndDiagnoseResourceWithRawResponse( - client.fix_parsing_and_diagnose - ) - self.fix = fix.AsyncFixResourceWithRawResponse(client.fix) + self._client = client + + @cached_property + def fixer(self) -> fixer.AsyncFixerResourceWithRawResponse: + from .resources.fixer import AsyncFixerResourceWithRawResponse + + return AsyncFixerResourceWithRawResponse(self._client.fixer) + + @cached_property + def stacks(self) -> stacks.AsyncStacksResourceWithRawResponse: + from .resources.stacks import AsyncStacksResourceWithRawResponse + + return AsyncStacksResourceWithRawResponse(self._client.stacks) + + @cached_property + def fix_string_literals(self) -> fix_string_literals.AsyncFixStringLiteralsResourceWithRawResponse: + from .resources.fix_string_literals import AsyncFixStringLiteralsResourceWithRawResponse + + return AsyncFixStringLiteralsResourceWithRawResponse(self._client.fix_string_literals) + + @cached_property + def validate_template(self) -> validate_template.AsyncValidateTemplateResourceWithRawResponse: + from .resources.validate_template import AsyncValidateTemplateResourceWithRawResponse + + return AsyncValidateTemplateResourceWithRawResponse(self._client.validate_template) + + @cached_property + def fix_parsing_and_diagnose(self) -> fix_parsing_and_diagnose.AsyncFixParsingAndDiagnoseResourceWithRawResponse: + from .resources.fix_parsing_and_diagnose import AsyncFixParsingAndDiagnoseResourceWithRawResponse + + return AsyncFixParsingAndDiagnoseResourceWithRawResponse(self._client.fix_parsing_and_diagnose) + + @cached_property + def fix(self) -> fix.AsyncFixResourceWithRawResponse: + from .resources.fix import AsyncFixResourceWithRawResponse + + return AsyncFixResourceWithRawResponse(self._client.fix) class BenchifyWithStreamedResponse: + _client: Benchify + def __init__(self, client: Benchify) -> None: - self.fixer = fixer.FixerResourceWithStreamingResponse(client.fixer) - self.stacks = stacks.StacksResourceWithStreamingResponse(client.stacks) - self.fix_string_literals = fix_string_literals.FixStringLiteralsResourceWithStreamingResponse( - client.fix_string_literals - ) - self.validate_template = validate_template.ValidateTemplateResourceWithStreamingResponse( - client.validate_template - ) - self.fix_parsing_and_diagnose = fix_parsing_and_diagnose.FixParsingAndDiagnoseResourceWithStreamingResponse( - client.fix_parsing_and_diagnose - ) - self.fix = fix.FixResourceWithStreamingResponse(client.fix) + self._client = client + + @cached_property + def fixer(self) -> fixer.FixerResourceWithStreamingResponse: + from .resources.fixer import FixerResourceWithStreamingResponse + + return FixerResourceWithStreamingResponse(self._client.fixer) + + @cached_property + def stacks(self) -> stacks.StacksResourceWithStreamingResponse: + from .resources.stacks import StacksResourceWithStreamingResponse + + return StacksResourceWithStreamingResponse(self._client.stacks) + + @cached_property + def fix_string_literals(self) -> fix_string_literals.FixStringLiteralsResourceWithStreamingResponse: + from .resources.fix_string_literals import FixStringLiteralsResourceWithStreamingResponse + + return FixStringLiteralsResourceWithStreamingResponse(self._client.fix_string_literals) + + @cached_property + def validate_template(self) -> validate_template.ValidateTemplateResourceWithStreamingResponse: + from .resources.validate_template import ValidateTemplateResourceWithStreamingResponse + + return ValidateTemplateResourceWithStreamingResponse(self._client.validate_template) + + @cached_property + def fix_parsing_and_diagnose(self) -> fix_parsing_and_diagnose.FixParsingAndDiagnoseResourceWithStreamingResponse: + from .resources.fix_parsing_and_diagnose import FixParsingAndDiagnoseResourceWithStreamingResponse + + return FixParsingAndDiagnoseResourceWithStreamingResponse(self._client.fix_parsing_and_diagnose) + + @cached_property + def fix(self) -> fix.FixResourceWithStreamingResponse: + from .resources.fix import FixResourceWithStreamingResponse + + return FixResourceWithStreamingResponse(self._client.fix) class AsyncBenchifyWithStreamedResponse: + _client: AsyncBenchify + def __init__(self, client: AsyncBenchify) -> None: - self.fixer = fixer.AsyncFixerResourceWithStreamingResponse(client.fixer) - self.stacks = stacks.AsyncStacksResourceWithStreamingResponse(client.stacks) - self.fix_string_literals = fix_string_literals.AsyncFixStringLiteralsResourceWithStreamingResponse( - client.fix_string_literals - ) - self.validate_template = validate_template.AsyncValidateTemplateResourceWithStreamingResponse( - client.validate_template - ) - self.fix_parsing_and_diagnose = ( - fix_parsing_and_diagnose.AsyncFixParsingAndDiagnoseResourceWithStreamingResponse( - client.fix_parsing_and_diagnose - ) - ) - self.fix = fix.AsyncFixResourceWithStreamingResponse(client.fix) + self._client = client + + @cached_property + def fixer(self) -> fixer.AsyncFixerResourceWithStreamingResponse: + from .resources.fixer import AsyncFixerResourceWithStreamingResponse + + return AsyncFixerResourceWithStreamingResponse(self._client.fixer) + + @cached_property + def stacks(self) -> stacks.AsyncStacksResourceWithStreamingResponse: + from .resources.stacks import AsyncStacksResourceWithStreamingResponse + + return AsyncStacksResourceWithStreamingResponse(self._client.stacks) + + @cached_property + def fix_string_literals(self) -> fix_string_literals.AsyncFixStringLiteralsResourceWithStreamingResponse: + from .resources.fix_string_literals import AsyncFixStringLiteralsResourceWithStreamingResponse + + return AsyncFixStringLiteralsResourceWithStreamingResponse(self._client.fix_string_literals) + + @cached_property + def validate_template(self) -> validate_template.AsyncValidateTemplateResourceWithStreamingResponse: + from .resources.validate_template import AsyncValidateTemplateResourceWithStreamingResponse + + return AsyncValidateTemplateResourceWithStreamingResponse(self._client.validate_template) + + @cached_property + def fix_parsing_and_diagnose( + self, + ) -> fix_parsing_and_diagnose.AsyncFixParsingAndDiagnoseResourceWithStreamingResponse: + from .resources.fix_parsing_and_diagnose import AsyncFixParsingAndDiagnoseResourceWithStreamingResponse + + return AsyncFixParsingAndDiagnoseResourceWithStreamingResponse(self._client.fix_parsing_and_diagnose) + + @cached_property + def fix(self) -> fix.AsyncFixResourceWithStreamingResponse: + from .resources.fix import AsyncFixResourceWithStreamingResponse + + return AsyncFixResourceWithStreamingResponse(self._client.fix) Client = Benchify diff --git a/src/benchify/_compat.py b/src/benchify/_compat.py index bdef67f0..786ff42a 100644 --- a/src/benchify/_compat.py +++ b/src/benchify/_compat.py @@ -139,6 +139,7 @@ def model_dump( exclude_defaults: bool = False, warnings: bool = True, mode: Literal["json", "python"] = "python", + by_alias: bool | None = None, ) -> dict[str, Any]: if (not PYDANTIC_V1) or hasattr(model, "model_dump"): return model.model_dump( @@ -148,13 +149,12 @@ def model_dump( exclude_defaults=exclude_defaults, # warnings are not supported in Pydantic v1 warnings=True if PYDANTIC_V1 else warnings, + by_alias=by_alias, ) return cast( "dict[str, Any]", model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast] - exclude=exclude, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, + exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, by_alias=bool(by_alias) ), ) diff --git a/src/benchify/_models.py b/src/benchify/_models.py index 6a3cd1d2..29070e05 100644 --- a/src/benchify/_models.py +++ b/src/benchify/_models.py @@ -2,7 +2,21 @@ import os import inspect -from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast +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, @@ -256,15 +270,16 @@ def model_dump( mode: Literal["json", "python"] | str = "python", include: IncEx | None = None, exclude: IncEx | None = None, + context: Any | None = None, by_alias: bool | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, + exclude_computed_fields: bool = False, round_trip: bool = False, warnings: bool | Literal["none", "warn", "error"] = True, - context: dict[str, Any] | None = None, - serialize_as_any: bool = False, fallback: Callable[[Any], Any] | None = None, + serialize_as_any: bool = False, ) -> dict[str, Any]: """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump @@ -272,16 +287,24 @@ def model_dump( Args: mode: The mode in which `to_python` should run. - If mode is 'json', the dictionary will only contain JSON serializable types. - If mode is 'python', the dictionary may contain any Python objects. - include: A list of fields to include in the output. - exclude: A list of fields to exclude from the output. + If mode is 'json', the output will only contain JSON serializable types. + If mode is 'python', the output may contain non-JSON-serializable Python objects. + include: A set of fields to include in the output. + exclude: A set of fields to exclude from the output. + context: Additional context to pass to the serializer. by_alias: Whether to use the field's alias in the dictionary key if defined. - exclude_unset: Whether to exclude fields that are unset or None from the output. - exclude_defaults: Whether to exclude fields that are set to their default value from the output. - exclude_none: Whether to exclude fields that have a value of `None` from the output. - round_trip: Whether to enable serialization and deserialization round-trip support. - warnings: Whether to log warnings when invalid fields are encountered. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that are set to their default value. + exclude_none: Whether to exclude fields that have a value of `None`. + exclude_computed_fields: Whether to exclude computed fields. + While this can be useful for round-tripping, it is usually recommended to use the dedicated + `round_trip` parameter instead. + round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T]. + warnings: How to handle serialization errors. False/"none" ignores them, True/"warn" logs errors, + "error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError]. + fallback: A function to call when an unknown value is encountered. If not provided, + a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised. + serialize_as_any: Whether to serialize fields with duck-typing serialization behavior. Returns: A dictionary representation of the model. @@ -298,6 +321,8 @@ def model_dump( raise ValueError("serialize_as_any is only supported in Pydantic v2") if fallback is not None: raise ValueError("fallback is only supported in Pydantic v2") + if exclude_computed_fields != False: + raise ValueError("exclude_computed_fields is only supported in Pydantic v2") dumped = super().dict( # pyright: ignore[reportDeprecated] include=include, exclude=exclude, @@ -314,15 +339,17 @@ def model_dump_json( self, *, indent: int | None = None, + ensure_ascii: bool = False, include: IncEx | None = None, exclude: IncEx | None = None, + context: Any | None = None, by_alias: bool | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, + exclude_computed_fields: bool = False, round_trip: bool = False, warnings: bool | Literal["none", "warn", "error"] = True, - context: dict[str, Any] | None = None, fallback: Callable[[Any], Any] | None = None, serialize_as_any: bool = False, ) -> str: @@ -354,6 +381,10 @@ def model_dump_json( raise ValueError("serialize_as_any is only supported in Pydantic v2") if fallback is not None: raise ValueError("fallback is only supported in Pydantic v2") + if ensure_ascii != False: + raise ValueError("ensure_ascii is only supported in Pydantic v2") + if exclude_computed_fields != False: + raise ValueError("exclude_computed_fields is only supported in Pydantic v2") return super().json( # type: ignore[reportDeprecated] indent=indent, include=include, @@ -573,6 +604,9 @@ class CachedDiscriminatorType(Protocol): __discriminator__: DiscriminatorDetails +DISCRIMINATOR_CACHE: weakref.WeakKeyDictionary[type, DiscriminatorDetails] = weakref.WeakKeyDictionary() + + class DiscriminatorDetails: field_name: str """The name of the discriminator field in the variant class, e.g. @@ -615,8 +649,9 @@ def __init__( def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, ...]) -> DiscriminatorDetails | None: - if isinstance(union, CachedDiscriminatorType): - return union.__discriminator__ + cached = DISCRIMINATOR_CACHE.get(union) + if cached is not None: + return cached discriminator_field_name: str | None = None @@ -669,7 +704,7 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, discriminator_field=discriminator_field_name, discriminator_alias=discriminator_alias, ) - cast(CachedDiscriminatorType, union).__discriminator__ = details + DISCRIMINATOR_CACHE.setdefault(union, details) return details @@ -765,6 +800,7 @@ class FinalRequestOptionsInput(TypedDict, total=False): timeout: float | Timeout | None files: HttpxRequestFiles | None idempotency_key: str + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] json_data: Body extra_json: AnyMapping follow_redirects: bool @@ -783,6 +819,7 @@ class FinalRequestOptions(pydantic.BaseModel): post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() follow_redirects: Union[bool, None] = None + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None # It should be noted that we cannot use `json` here as that would override # a BaseModel method in an incompatible fashion. json_data: Union[Body, None] = None diff --git a/src/benchify/_response.py b/src/benchify/_response.py index 31df498c..40887001 100644 --- a/src/benchify/_response.py +++ b/src/benchify/_response.py @@ -152,6 +152,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: ), response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) @@ -162,6 +163,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: cast_to=extract_stream_chunk_type(self._stream_cls), response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) @@ -175,6 +177,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: cast_to=cast_to, response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) diff --git a/src/benchify/_streaming.py b/src/benchify/_streaming.py index de5651fa..5a2a4bb9 100644 --- a/src/benchify/_streaming.py +++ b/src/benchify/_streaming.py @@ -4,7 +4,7 @@ import json import inspect from types import TracebackType -from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, AsyncIterator, cast +from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, Optional, AsyncIterator, cast from typing_extensions import Self, Protocol, TypeGuard, override, get_origin, runtime_checkable import httpx @@ -13,6 +13,7 @@ if TYPE_CHECKING: from ._client import Benchify, AsyncBenchify + from ._models import FinalRequestOptions _T = TypeVar("_T") @@ -22,7 +23,7 @@ class Stream(Generic[_T]): """Provides the core interface to iterate over a synchronous stream response.""" response: httpx.Response - + _options: Optional[FinalRequestOptions] = None _decoder: SSEBytesDecoder def __init__( @@ -31,10 +32,12 @@ def __init__( cast_to: type[_T], response: httpx.Response, client: Benchify, + 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__() @@ -54,11 +57,12 @@ def __stream__(self) -> Iterator[_T]: process_data = self._client._process_response_data iterator = self._iter_events() - for sse in iterator: - yield process_data(data=sse.json(), cast_to=cast_to, response=response) - - # As we might not fully consume the response stream, we need to close it explicitly - response.close() + try: + for sse in iterator: + yield process_data(data=sse.json(), cast_to=cast_to, response=response) + finally: + # Ensure the response is closed even if the consumer doesn't read all data + response.close() def __enter__(self) -> Self: return self @@ -84,7 +88,7 @@ class AsyncStream(Generic[_T]): """Provides the core interface to iterate over an asynchronous stream response.""" response: httpx.Response - + _options: Optional[FinalRequestOptions] = None _decoder: SSEDecoder | SSEBytesDecoder def __init__( @@ -93,10 +97,12 @@ def __init__( cast_to: type[_T], response: httpx.Response, client: AsyncBenchify, + 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__() @@ -117,11 +123,12 @@ async def __stream__(self) -> AsyncIterator[_T]: process_data = self._client._process_response_data iterator = self._iter_events() - async for sse in iterator: - yield process_data(data=sse.json(), cast_to=cast_to, response=response) - - # As we might not fully consume the response stream, we need to close it explicitly - await response.aclose() + try: + async for sse in iterator: + yield process_data(data=sse.json(), cast_to=cast_to, response=response) + finally: + # Ensure the response is closed even if the consumer doesn't read all data + await response.aclose() async def __aenter__(self) -> Self: return self diff --git a/src/benchify/_types.py b/src/benchify/_types.py index 4acf9272..c452a1ea 100644 --- a/src/benchify/_types.py +++ b/src/benchify/_types.py @@ -13,9 +13,11 @@ Mapping, TypeVar, Callable, + Iterable, Iterator, Optional, Sequence, + AsyncIterable, ) from typing_extensions import ( Set, @@ -56,6 +58,13 @@ else: Base64FileInput = Union[IO[bytes], PathLike] FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8. + + +# Used for sending raw binary data / streaming data in request bodies +# e.g. for file uploads without multipart encoding +BinaryTypes = Union[bytes, bytearray, IO[bytes], Iterable[bytes]] +AsyncBinaryTypes = Union[bytes, bytearray, IO[bytes], AsyncIterable[bytes]] + FileTypes = Union[ # file (or bytes) FileContent, @@ -243,6 +252,9 @@ class HttpxSendArgs(TypedDict, total=False): if TYPE_CHECKING: # This works because str.__contains__ does not accept object (either in typeshed or at runtime) # https://github.com/hauntsaninja/useful_types/blob/5e9710f3875107d068e7679fd7fec9cfab0eff3b/useful_types/__init__.py#L285 + # + # Note: index() and count() methods are intentionally omitted to allow pyright to properly + # infer TypedDict types when dict literals are used in lists assigned to SequenceNotStr. class SequenceNotStr(Protocol[_T_co]): @overload def __getitem__(self, index: SupportsIndex, /) -> _T_co: ... @@ -251,8 +263,6 @@ def __getitem__(self, index: slice, /) -> Sequence[_T_co]: ... def __contains__(self, value: object, /) -> bool: ... def __len__(self) -> int: ... def __iter__(self) -> Iterator[_T_co]: ... - def index(self, value: Any, start: int = 0, stop: int = ..., /) -> int: ... - def count(self, value: Any, /) -> int: ... def __reversed__(self) -> Iterator[_T_co]: ... else: # just point this to a normal `Sequence` at runtime to avoid having to special case diff --git a/src/benchify/_utils/_compat.py b/src/benchify/_utils/_compat.py index dd703233..2c70b299 100644 --- a/src/benchify/_utils/_compat.py +++ b/src/benchify/_utils/_compat.py @@ -26,7 +26,7 @@ def is_union(tp: Optional[Type[Any]]) -> bool: else: import types - return tp is Union or tp is types.UnionType + return tp is Union or tp is types.UnionType # type: ignore[comparison-overlap] def is_typeddict(tp: Type[Any]) -> bool: diff --git a/src/benchify/_utils/_json.py b/src/benchify/_utils/_json.py new file mode 100644 index 00000000..60584214 --- /dev/null +++ b/src/benchify/_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/benchify/_utils/_sync.py b/src/benchify/_utils/_sync.py index ad7ec71b..f6027c18 100644 --- a/src/benchify/_utils/_sync.py +++ b/src/benchify/_utils/_sync.py @@ -1,10 +1,8 @@ from __future__ import annotations -import sys import asyncio import functools -import contextvars -from typing import Any, TypeVar, Callable, Awaitable +from typing import TypeVar, Callable, Awaitable from typing_extensions import ParamSpec import anyio @@ -15,34 +13,11 @@ T_ParamSpec = ParamSpec("T_ParamSpec") -if sys.version_info >= (3, 9): - _asyncio_to_thread = asyncio.to_thread -else: - # backport of https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread - # for Python 3.8 support - async def _asyncio_to_thread( - func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs - ) -> Any: - """Asynchronously run function *func* in a separate thread. - - Any *args and **kwargs supplied for this function are directly passed - to *func*. Also, the current :class:`contextvars.Context` is propagated, - allowing context variables from the main thread to be accessed in the - separate thread. - - Returns a coroutine that can be awaited to get the eventual result of *func*. - """ - loop = asyncio.events.get_running_loop() - ctx = contextvars.copy_context() - func_call = functools.partial(ctx.run, func, *args, **kwargs) - return await loop.run_in_executor(None, func_call) - - async def to_thread( func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs ) -> T_Retval: if sniffio.current_async_library() == "asyncio": - return await _asyncio_to_thread(func, *args, **kwargs) + return await asyncio.to_thread(func, *args, **kwargs) return await anyio.to_thread.run_sync( functools.partial(func, *args, **kwargs), @@ -53,10 +28,7 @@ async def to_thread( def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]: """ Take a blocking function and create an async one that receives the same - positional and keyword arguments. For python version 3.9 and above, it uses - asyncio.to_thread to run the function in a separate thread. For python version - 3.8, it uses locally defined copy of the asyncio.to_thread function which was - introduced in python 3.9. + positional and keyword arguments. Usage: diff --git a/src/benchify/_version.py b/src/benchify/_version.py index a0d071c3..4381b8d4 100644 --- a/src/benchify/_version.py +++ b/src/benchify/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "benchify" -__version__ = "0.7.0" # x-release-please-version +__version__ = "0.8.0" # x-release-please-version diff --git a/src/benchify/resources/fix/fix.py b/src/benchify/resources/fix/fix.py index 87addf89..539c5241 100644 --- a/src/benchify/resources/fix/fix.py +++ b/src/benchify/resources/fix/fix.py @@ -60,11 +60,12 @@ def create_ai_fallback( *, files: Iterable[fix_create_ai_fallback_params.File], remaining_diagnostics: fix_create_ai_fallback_params.RemainingDiagnostics, - template_path: str, + continuation_event_id: str | Omit = omit, event_id: str | Omit = omit, include_context: bool | Omit = omit, max_attempts: float | Omit = omit, meta: Optional[fix_create_ai_fallback_params.Meta] | Omit = omit, + template_path: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -83,7 +84,7 @@ def create_ai_fallback( remaining_diagnostics: Diagnostics that remain after standard fixing - template_path: Full path to the template + continuation_event_id: Event ID from Step 1 to continue with the same temp directory event_id: Unique identifier for the event @@ -93,6 +94,9 @@ def create_ai_fallback( meta: Meta information for the request + template_path: Template path for project context (defaults to benchify/default-template if not + provided) + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -107,11 +111,12 @@ def create_ai_fallback( { "files": files, "remaining_diagnostics": remaining_diagnostics, - "template_path": template_path, + "continuation_event_id": continuation_event_id, "event_id": event_id, "include_context": include_context, "max_attempts": max_attempts, "meta": meta, + "template_path": template_path, }, fix_create_ai_fallback_params.FixCreateAIFallbackParams, ), @@ -151,11 +156,12 @@ async def create_ai_fallback( *, files: Iterable[fix_create_ai_fallback_params.File], remaining_diagnostics: fix_create_ai_fallback_params.RemainingDiagnostics, - template_path: str, + continuation_event_id: str | Omit = omit, event_id: str | Omit = omit, include_context: bool | Omit = omit, max_attempts: float | Omit = omit, meta: Optional[fix_create_ai_fallback_params.Meta] | Omit = omit, + template_path: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -174,7 +180,7 @@ async def create_ai_fallback( remaining_diagnostics: Diagnostics that remain after standard fixing - template_path: Full path to the template + continuation_event_id: Event ID from Step 1 to continue with the same temp directory event_id: Unique identifier for the event @@ -184,6 +190,9 @@ async def create_ai_fallback( meta: Meta information for the request + template_path: Template path for project context (defaults to benchify/default-template if not + provided) + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -198,11 +207,12 @@ async def create_ai_fallback( { "files": files, "remaining_diagnostics": remaining_diagnostics, - "template_path": template_path, + "continuation_event_id": continuation_event_id, "event_id": event_id, "include_context": include_context, "max_attempts": max_attempts, "meta": meta, + "template_path": template_path, }, fix_create_ai_fallback_params.FixCreateAIFallbackParams, ), diff --git a/src/benchify/resources/fix/standard.py b/src/benchify/resources/fix/standard.py index f8a3632c..3d4125d3 100644 --- a/src/benchify/resources/fix/standard.py +++ b/src/benchify/resources/fix/standard.py @@ -50,6 +50,7 @@ def create( files: Iterable[standard_create_params.File], remaining_diagnostics: standard_create_params.RemainingDiagnostics, bundle: bool | Omit = omit, + continuation_event_id: str | Omit = omit, event_id: str | Omit = omit, fix_types: List[Literal["dependency", "parsing", "css", "ai_fallback", "types", "ui", "sql"]] | Omit = omit, meta: Optional[standard_create_params.Meta] | Omit = omit, @@ -76,6 +77,8 @@ def create( bundle: Whether to bundle the project after fixes + continuation_event_id: Event ID from Step 1 to continue with the same temp directory + event_id: Unique identifier for tracking fix_types: Types of standard fixes to apply @@ -101,6 +104,7 @@ def create( "files": files, "remaining_diagnostics": remaining_diagnostics, "bundle": bundle, + "continuation_event_id": continuation_event_id, "event_id": event_id, "fix_types": fix_types, "meta": meta, @@ -142,6 +146,7 @@ async def create( files: Iterable[standard_create_params.File], remaining_diagnostics: standard_create_params.RemainingDiagnostics, bundle: bool | Omit = omit, + continuation_event_id: str | Omit = omit, event_id: str | Omit = omit, fix_types: List[Literal["dependency", "parsing", "css", "ai_fallback", "types", "ui", "sql"]] | Omit = omit, meta: Optional[standard_create_params.Meta] | Omit = omit, @@ -168,6 +173,8 @@ async def create( bundle: Whether to bundle the project after fixes + continuation_event_id: Event ID from Step 1 to continue with the same temp directory + event_id: Unique identifier for tracking fix_types: Types of standard fixes to apply @@ -193,6 +200,7 @@ async def create( "files": files, "remaining_diagnostics": remaining_diagnostics, "bundle": bundle, + "continuation_event_id": continuation_event_id, "event_id": event_id, "fix_types": fix_types, "meta": meta, diff --git a/src/benchify/resources/stacks.py b/src/benchify/resources/stacks.py index 7abadedd..13a842db 100644 --- a/src/benchify/resources/stacks.py +++ b/src/benchify/resources/stacks.py @@ -13,8 +13,8 @@ stack_get_logs_params, stack_read_file_params, stack_write_file_params, - stack_create_and_run_params, stack_execute_command_params, + stack_bundle_multipart_params, stack_wait_for_dev_server_url_params, ) from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, FileTypes, SequenceNotStr, omit, not_given @@ -35,8 +35,8 @@ from ..types.stack_retrieve_response import StackRetrieveResponse from ..types.stack_read_file_response import StackReadFileResponse from ..types.stack_write_file_response import StackWriteFileResponse -from ..types.stack_create_and_run_response import StackCreateAndRunResponse from ..types.stack_execute_command_response import StackExecuteCommandResponse +from ..types.stack_bundle_multipart_response import StackBundleMultipartResponse from ..types.stack_get_network_info_response import StackGetNetworkInfoResponse from ..types.stack_wait_for_dev_server_url_response import StackWaitForDevServerURLResponse @@ -244,31 +244,27 @@ def update( cast_to=StackUpdateResponse, ) - def create_and_run( + def bundle_multipart( self, *, - command: SequenceNotStr[str], - image: str, - ttl_seconds: float | Omit = omit, - wait: bool | Omit = omit, + manifest: str, + tarball: FileTypes, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> StackCreateAndRunResponse: + ) -> StackBundleMultipartResponse: """ - Create a simple container sandbox with a custom image and command + Accepts multipart/form-data containing a JSON string manifest (must include + entrypoint) and a tarball file, forwards to /sandbox/bundle-multipart, and + returns base64 bundle (path + content). Args: - command: Command to run - - image: Docker image to use - - ttl_seconds: Time to live in seconds + manifest: JSON string containing bundler manifest (must include entrypoint) - wait: Wait for container to be ready + tarball: Tar.zst project archive extra_headers: Send extra headers @@ -278,21 +274,25 @@ def create_and_run( timeout: Override the client-level default timeout for this request, in seconds """ + body = deepcopy_minimal( + { + "manifest": manifest, + "tarball": tarball, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["tarball"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} return self._post( - "/v1/stacks/create-and-run", - body=maybe_transform( - { - "command": command, - "image": image, - "ttl_seconds": ttl_seconds, - "wait": wait, - }, - stack_create_and_run_params.StackCreateAndRunParams, - ), + "/v1/stacks/bundle-multipart", + body=maybe_transform(body, stack_bundle_multipart_params.StackBundleMultipartParams), + files=files, options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=StackCreateAndRunResponse, + cast_to=StackBundleMultipartResponse, ) def destroy( @@ -841,31 +841,27 @@ async def update( cast_to=StackUpdateResponse, ) - async def create_and_run( + async def bundle_multipart( self, *, - command: SequenceNotStr[str], - image: str, - ttl_seconds: float | Omit = omit, - wait: bool | Omit = omit, + manifest: str, + tarball: FileTypes, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> StackCreateAndRunResponse: + ) -> StackBundleMultipartResponse: """ - Create a simple container sandbox with a custom image and command + Accepts multipart/form-data containing a JSON string manifest (must include + entrypoint) and a tarball file, forwards to /sandbox/bundle-multipart, and + returns base64 bundle (path + content). Args: - command: Command to run - - image: Docker image to use - - ttl_seconds: Time to live in seconds + manifest: JSON string containing bundler manifest (must include entrypoint) - wait: Wait for container to be ready + tarball: Tar.zst project archive extra_headers: Send extra headers @@ -875,21 +871,25 @@ async def create_and_run( timeout: Override the client-level default timeout for this request, in seconds """ + body = deepcopy_minimal( + { + "manifest": manifest, + "tarball": tarball, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["tarball"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} return await self._post( - "/v1/stacks/create-and-run", - body=await async_maybe_transform( - { - "command": command, - "image": image, - "ttl_seconds": ttl_seconds, - "wait": wait, - }, - stack_create_and_run_params.StackCreateAndRunParams, - ), + "/v1/stacks/bundle-multipart", + body=await async_maybe_transform(body, stack_bundle_multipart_params.StackBundleMultipartParams), + files=files, options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=StackCreateAndRunResponse, + cast_to=StackBundleMultipartResponse, ) async def destroy( @@ -1252,8 +1252,8 @@ def __init__(self, stacks: StacksResource) -> None: self.update = to_raw_response_wrapper( stacks.update, ) - self.create_and_run = to_raw_response_wrapper( - stacks.create_and_run, + self.bundle_multipart = to_raw_response_wrapper( + stacks.bundle_multipart, ) self.destroy = to_raw_response_wrapper( stacks.destroy, @@ -1294,8 +1294,8 @@ def __init__(self, stacks: AsyncStacksResource) -> None: self.update = async_to_raw_response_wrapper( stacks.update, ) - self.create_and_run = async_to_raw_response_wrapper( - stacks.create_and_run, + self.bundle_multipart = async_to_raw_response_wrapper( + stacks.bundle_multipart, ) self.destroy = async_to_raw_response_wrapper( stacks.destroy, @@ -1336,8 +1336,8 @@ def __init__(self, stacks: StacksResource) -> None: self.update = to_streamed_response_wrapper( stacks.update, ) - self.create_and_run = to_streamed_response_wrapper( - stacks.create_and_run, + self.bundle_multipart = to_streamed_response_wrapper( + stacks.bundle_multipart, ) self.destroy = to_streamed_response_wrapper( stacks.destroy, @@ -1378,8 +1378,8 @@ def __init__(self, stacks: AsyncStacksResource) -> None: self.update = async_to_streamed_response_wrapper( stacks.update, ) - self.create_and_run = async_to_streamed_response_wrapper( - stacks.create_and_run, + self.bundle_multipart = async_to_streamed_response_wrapper( + stacks.bundle_multipart, ) self.destroy = async_to_streamed_response_wrapper( stacks.destroy, diff --git a/src/benchify/types/__init__.py b/src/benchify/types/__init__.py index ee71242c..f4a76137 100644 --- a/src/benchify/types/__init__.py +++ b/src/benchify/types/__init__.py @@ -17,12 +17,12 @@ from .stack_write_file_params import StackWriteFileParams as StackWriteFileParams from .stack_read_file_response import StackReadFileResponse as StackReadFileResponse from .stack_write_file_response import StackWriteFileResponse as StackWriteFileResponse -from .stack_create_and_run_params import StackCreateAndRunParams as StackCreateAndRunParams from .stack_execute_command_params import StackExecuteCommandParams as StackExecuteCommandParams from .fix_create_ai_fallback_params import FixCreateAIFallbackParams as FixCreateAIFallbackParams -from .stack_create_and_run_response import StackCreateAndRunResponse as StackCreateAndRunResponse +from .stack_bundle_multipart_params import StackBundleMultipartParams as StackBundleMultipartParams from .stack_execute_command_response import StackExecuteCommandResponse as StackExecuteCommandResponse from .fix_create_ai_fallback_response import FixCreateAIFallbackResponse as FixCreateAIFallbackResponse +from .stack_bundle_multipart_response import StackBundleMultipartResponse as StackBundleMultipartResponse from .stack_get_network_info_response import StackGetNetworkInfoResponse as StackGetNetworkInfoResponse from .fix_string_literal_create_params import FixStringLiteralCreateParams as FixStringLiteralCreateParams from .validate_template_validate_params import ValidateTemplateValidateParams as ValidateTemplateValidateParams diff --git a/src/benchify/types/fix/standard_create_params.py b/src/benchify/types/fix/standard_create_params.py index c7f570c2..9c86e1b8 100644 --- a/src/benchify/types/fix/standard_create_params.py +++ b/src/benchify/types/fix/standard_create_params.py @@ -25,6 +25,9 @@ class StandardCreateParams(TypedDict, total=False): bundle: bool """Whether to bundle the project after fixes""" + continuation_event_id: str + """Event ID from Step 1 to continue with the same temp directory""" + event_id: str """Unique identifier for tracking""" @@ -50,6 +53,8 @@ class File(TypedDict, total=False): class RemainingDiagnosticsFileToDiagnosticLocation(TypedDict, total=False): + """Location of the diagnostic""" + column: Required[Optional[float]] """Column number (1-based)""" @@ -84,10 +89,14 @@ class RemainingDiagnosticsFileToDiagnostic(TypedDict, total=False): class RemainingDiagnostics(TypedDict, total=False): + """Diagnostics to fix (output from step 1 or previous fixes)""" + file_to_diagnostics: Dict[str, Iterable[RemainingDiagnosticsFileToDiagnostic]] """Diagnostics grouped by file""" class Meta(TypedDict, total=False): + """Meta information for the request""" + external_id: Optional[str] """Customer tracking identifier""" diff --git a/src/benchify/types/fix/standard_create_response.py b/src/benchify/types/fix/standard_create_response.py index 9e80c022..4be43481 100644 --- a/src/benchify/types/fix/standard_create_response.py +++ b/src/benchify/types/fix/standard_create_response.py @@ -27,6 +27,8 @@ class DataChangedFile(BaseModel): class DataRemainingDiagnosticsFileToDiagnosticLocation(BaseModel): + """Location of the diagnostic""" + column: Optional[float] = None """Column number (1-based)""" @@ -61,6 +63,8 @@ class DataRemainingDiagnosticsFileToDiagnostic(BaseModel): class DataRemainingDiagnostics(BaseModel): + """Remaining diagnostics after standard fixes""" + file_to_diagnostics: Optional[Dict[str, List[DataRemainingDiagnosticsFileToDiagnostic]]] = None """Diagnostics grouped by file""" @@ -74,6 +78,8 @@ class DataBundledFile(BaseModel): class Data(BaseModel): + """The actual response data""" + changed_files: List[DataChangedFile] """Files that were modified during fixing""" @@ -103,6 +109,8 @@ class Data(BaseModel): class Error(BaseModel): + """The error from the API query""" + code: str """The error code""" @@ -114,6 +122,8 @@ class Error(BaseModel): class Meta(BaseModel): + """Meta information""" + external_id: Optional[str] = None """Customer tracking identifier""" diff --git a/src/benchify/types/fix_create_ai_fallback_params.py b/src/benchify/types/fix_create_ai_fallback_params.py index 335cd397..4f005fc3 100644 --- a/src/benchify/types/fix_create_ai_fallback_params.py +++ b/src/benchify/types/fix_create_ai_fallback_params.py @@ -22,8 +22,8 @@ class FixCreateAIFallbackParams(TypedDict, total=False): remaining_diagnostics: Required[RemainingDiagnostics] """Diagnostics that remain after standard fixing""" - template_path: Required[str] - """Full path to the template""" + continuation_event_id: str + """Event ID from Step 1 to continue with the same temp directory""" event_id: str """Unique identifier for the event""" @@ -37,6 +37,12 @@ class FixCreateAIFallbackParams(TypedDict, total=False): meta: Optional[Meta] """Meta information for the request""" + template_path: str + """ + Template path for project context (defaults to benchify/default-template if not + provided) + """ + class File(TypedDict, total=False): contents: Required[str] @@ -47,6 +53,8 @@ class File(TypedDict, total=False): class RemainingDiagnosticsFileToDiagnosticLocation(TypedDict, total=False): + """Location of the diagnostic""" + column: Required[Optional[float]] """Column number (1-based)""" @@ -81,10 +89,14 @@ class RemainingDiagnosticsFileToDiagnostic(TypedDict, total=False): class RemainingDiagnostics(TypedDict, total=False): + """Diagnostics that remain after standard fixing""" + file_to_diagnostics: Dict[str, Iterable[RemainingDiagnosticsFileToDiagnostic]] """Diagnostics grouped by file""" class Meta(TypedDict, total=False): + """Meta information for the request""" + external_id: Optional[str] """Customer tracking identifier""" diff --git a/src/benchify/types/fix_create_ai_fallback_response.py b/src/benchify/types/fix_create_ai_fallback_response.py index 6e520d4b..6c25ddd5 100644 --- a/src/benchify/types/fix_create_ai_fallback_response.py +++ b/src/benchify/types/fix_create_ai_fallback_response.py @@ -25,6 +25,8 @@ class DataFileResults(BaseModel): class Data(BaseModel): + """The actual response data""" + execution_time: float """Time taken to execute AI fallback in seconds""" @@ -51,6 +53,8 @@ class Data(BaseModel): class Error(BaseModel): + """The error from the API query""" + code: str """The error code""" @@ -62,6 +66,8 @@ class Error(BaseModel): class Meta(BaseModel): + """Meta information""" + external_id: Optional[str] = None """Customer tracking identifier""" diff --git a/src/benchify/types/fix_parsing_and_diagnose_detect_issues_params.py b/src/benchify/types/fix_parsing_and_diagnose_detect_issues_params.py index 1e243bb3..da75a90e 100644 --- a/src/benchify/types/fix_parsing_and_diagnose_detect_issues_params.py +++ b/src/benchify/types/fix_parsing_and_diagnose_detect_issues_params.py @@ -34,5 +34,7 @@ class File(TypedDict, total=False): class Meta(TypedDict, total=False): + """Meta information for the request""" + external_id: Optional[str] """Customer tracking identifier""" diff --git a/src/benchify/types/fix_parsing_and_diagnose_detect_issues_response.py b/src/benchify/types/fix_parsing_and_diagnose_detect_issues_response.py index 2320cd02..fc610922 100644 --- a/src/benchify/types/fix_parsing_and_diagnose_detect_issues_response.py +++ b/src/benchify/types/fix_parsing_and_diagnose_detect_issues_response.py @@ -30,6 +30,8 @@ class DataChangedFile(BaseModel): class DataDiagnosticsNotRequestedFileToDiagnosticLocation(BaseModel): + """Location of the diagnostic""" + column: Optional[float] = None """Column number (1-based)""" @@ -64,11 +66,15 @@ class DataDiagnosticsNotRequestedFileToDiagnostic(BaseModel): class DataDiagnosticsNotRequested(BaseModel): + """Diagnostics that do not match the requested fix types""" + file_to_diagnostics: Optional[Dict[str, List[DataDiagnosticsNotRequestedFileToDiagnostic]]] = None """Diagnostics grouped by file""" class DataDiagnosticsRequestedFileToDiagnosticLocation(BaseModel): + """Location of the diagnostic""" + column: Optional[float] = None """Column number (1-based)""" @@ -103,11 +109,15 @@ class DataDiagnosticsRequestedFileToDiagnostic(BaseModel): class DataDiagnosticsRequested(BaseModel): + """Diagnostics that match the requested fix types""" + file_to_diagnostics: Optional[Dict[str, List[DataDiagnosticsRequestedFileToDiagnostic]]] = None """Diagnostics grouped by file""" class DataDiagnostics(BaseModel): + """Diagnostics split into fixable (requested) and other (not_requested) groups""" + not_requested: Optional[DataDiagnosticsNotRequested] = None """Diagnostics that do not match the requested fix types""" @@ -130,6 +140,8 @@ class DataFixTypesAvailable(BaseModel): class Data(BaseModel): + """The actual response data""" + changed_files: List[DataChangedFile] """Files that were changed during detection""" @@ -145,6 +157,9 @@ class Data(BaseModel): estimated_total_fix_time: float """Estimated total time to fix all issues in seconds""" + event_id: str + """Event ID for tracking this operation across steps""" + files_analyzed: float """Number of files that were analyzed""" @@ -165,6 +180,8 @@ class Data(BaseModel): class Error(BaseModel): + """The error from the API query""" + code: str """The error code""" @@ -176,6 +193,8 @@ class Error(BaseModel): class Meta(BaseModel): + """Meta information""" + external_id: Optional[str] = None """Customer tracking identifier""" diff --git a/src/benchify/types/fix_string_literal_create_params.py b/src/benchify/types/fix_string_literal_create_params.py index c1aa237b..7b7a273d 100644 --- a/src/benchify/types/fix_string_literal_create_params.py +++ b/src/benchify/types/fix_string_literal_create_params.py @@ -20,6 +20,8 @@ class FixStringLiteralCreateParams(TypedDict, total=False): class File(TypedDict, total=False): + """File to process""" + contents: Required[str] """File contents""" @@ -28,5 +30,7 @@ class File(TypedDict, total=False): class Meta(TypedDict, total=False): + """Meta information for the request""" + external_id: Optional[str] """Customer tracking identifier""" diff --git a/src/benchify/types/fix_string_literal_create_response.py b/src/benchify/types/fix_string_literal_create_response.py index 31e0c632..716d2c6e 100644 --- a/src/benchify/types/fix_string_literal_create_response.py +++ b/src/benchify/types/fix_string_literal_create_response.py @@ -19,6 +19,8 @@ class DataStrategyStatistic(BaseModel): class Data(BaseModel): + """The actual response data""" + contents: str """The file contents (original or fixed)""" @@ -36,6 +38,8 @@ class Data(BaseModel): class Error(BaseModel): + """The error from the API query""" + code: str """The error code""" @@ -47,6 +51,8 @@ class Error(BaseModel): class Meta(BaseModel): + """Meta information""" + external_id: Optional[str] = None """Customer tracking identifier""" diff --git a/src/benchify/types/fixer_run_params.py b/src/benchify/types/fixer_run_params.py index 1efbe2f1..d7858f4d 100644 --- a/src/benchify/types/fixer_run_params.py +++ b/src/benchify/types/fixer_run_params.py @@ -55,5 +55,7 @@ class File(TypedDict, total=False): class Meta(TypedDict, total=False): + """Meta information for the request""" + external_id: Optional[str] """Customer tracking identifier""" diff --git a/src/benchify/types/fixer_run_response.py b/src/benchify/types/fixer_run_response.py index 05e34043..064f3dc6 100644 --- a/src/benchify/types/fixer_run_response.py +++ b/src/benchify/types/fixer_run_response.py @@ -35,6 +35,8 @@ class DataStatus(BaseModel): + """Final per-file status after fixing""" + composite_status: Literal[ "FIXED_EVERYTHING", "FIXED_REQUESTED", "PARTIALLY_FIXED", "NO_REQUESTED_ISSUES", "NO_ISSUES", "FAILED" ] @@ -68,6 +70,8 @@ class DataSuggestedChangesChangedFile(BaseModel): class DataSuggestedChanges(BaseModel): + """Suggested changes to fix the issues""" + all_files: Optional[List[DataSuggestedChangesAllFile]] = None """List of all files with their current contents. @@ -93,6 +97,8 @@ class DataBundleFile(BaseModel): class DataBundle(BaseModel): + """Bundle information if bundling was requested""" + build_system: str status: Literal["SUCCESS", "FAILED", "NOT_ATTEMPTED", "PARTIAL_SUCCESS"] @@ -117,6 +123,8 @@ class DataFileToStrategyStatistic(BaseModel): class DataFinalDiagnosticsNotRequestedFileToDiagnosticLocation(BaseModel): + """Location of the diagnostic""" + column: Optional[float] = None """Column number (1-based)""" @@ -151,11 +159,15 @@ class DataFinalDiagnosticsNotRequestedFileToDiagnostic(BaseModel): class DataFinalDiagnosticsNotRequested(BaseModel): + """Diagnostics that do not match the requested fix types""" + file_to_diagnostics: Optional[Dict[str, List[DataFinalDiagnosticsNotRequestedFileToDiagnostic]]] = None """Diagnostics grouped by file""" class DataFinalDiagnosticsRequestedFileToDiagnosticLocation(BaseModel): + """Location of the diagnostic""" + column: Optional[float] = None """Column number (1-based)""" @@ -190,11 +202,17 @@ class DataFinalDiagnosticsRequestedFileToDiagnostic(BaseModel): class DataFinalDiagnosticsRequested(BaseModel): + """Diagnostics that match the requested fix types""" + file_to_diagnostics: Optional[Dict[str, List[DataFinalDiagnosticsRequestedFileToDiagnostic]]] = None """Diagnostics grouped by file""" class DataFinalDiagnostics(BaseModel): + """ + Diagnostics after fixing, split into relevant vs other based on requested fix types + """ + not_requested: Optional[DataFinalDiagnosticsNotRequested] = None """Diagnostics that do not match the requested fix types""" @@ -203,6 +221,8 @@ class DataFinalDiagnostics(BaseModel): class DataInitialDiagnosticsNotRequestedFileToDiagnosticLocation(BaseModel): + """Location of the diagnostic""" + column: Optional[float] = None """Column number (1-based)""" @@ -237,11 +257,15 @@ class DataInitialDiagnosticsNotRequestedFileToDiagnostic(BaseModel): class DataInitialDiagnosticsNotRequested(BaseModel): + """Diagnostics that do not match the requested fix types""" + file_to_diagnostics: Optional[Dict[str, List[DataInitialDiagnosticsNotRequestedFileToDiagnostic]]] = None """Diagnostics grouped by file""" class DataInitialDiagnosticsRequestedFileToDiagnosticLocation(BaseModel): + """Location of the diagnostic""" + column: Optional[float] = None """Column number (1-based)""" @@ -276,11 +300,17 @@ class DataInitialDiagnosticsRequestedFileToDiagnostic(BaseModel): class DataInitialDiagnosticsRequested(BaseModel): + """Diagnostics that match the requested fix types""" + file_to_diagnostics: Optional[Dict[str, List[DataInitialDiagnosticsRequestedFileToDiagnostic]]] = None """Diagnostics grouped by file""" class DataInitialDiagnostics(BaseModel): + """ + Diagnostics before fixing, split into relevant vs other based on requested fix types + """ + not_requested: Optional[DataInitialDiagnosticsNotRequested] = None """Diagnostics that do not match the requested fix types""" @@ -289,6 +319,8 @@ class DataInitialDiagnostics(BaseModel): class Data(BaseModel): + """The actual response data""" + fixer_version: str """Version of the fixer""" @@ -321,6 +353,8 @@ class Data(BaseModel): class Error(BaseModel): + """The error from the API query""" + code: str """The error code""" @@ -332,6 +366,8 @@ class Error(BaseModel): class Meta(BaseModel): + """Meta information""" + external_id: Optional[str] = None """Customer tracking identifier""" diff --git a/src/benchify/types/stack_bundle_multipart_params.py b/src/benchify/types/stack_bundle_multipart_params.py new file mode 100644 index 00000000..f12ab3f1 --- /dev/null +++ b/src/benchify/types/stack_bundle_multipart_params.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +from .._types import FileTypes + +__all__ = ["StackBundleMultipartParams"] + + +class StackBundleMultipartParams(TypedDict, total=False): + manifest: Required[str] + """JSON string containing bundler manifest (must include entrypoint)""" + + tarball: Required[FileTypes] + """Tar.zst project archive""" diff --git a/src/benchify/types/stack_bundle_multipart_response.py b/src/benchify/types/stack_bundle_multipart_response.py new file mode 100644 index 00000000..8a8e138b --- /dev/null +++ b/src/benchify/types/stack_bundle_multipart_response.py @@ -0,0 +1,27 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel + +__all__ = ["StackBundleMultipartResponse", "Manifest", "ManifestFile"] + + +class ManifestFile(BaseModel): + contents: str + + path: str + + +class Manifest(BaseModel): + files: List[ManifestFile] + + url: Optional[str] = None + + +class StackBundleMultipartResponse(BaseModel): + content: str + + path: str + + manifest: Optional[Manifest] = None diff --git a/src/benchify/types/stack_create_and_run_params.py b/src/benchify/types/stack_create_and_run_params.py deleted file mode 100644 index 76ba5c47..00000000 --- a/src/benchify/types/stack_create_and_run_params.py +++ /dev/null @@ -1,23 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Required, TypedDict - -from .._types import SequenceNotStr - -__all__ = ["StackCreateAndRunParams"] - - -class StackCreateAndRunParams(TypedDict, total=False): - command: Required[SequenceNotStr[str]] - """Command to run""" - - image: Required[str] - """Docker image to use""" - - ttl_seconds: float - """Time to live in seconds""" - - wait: bool - """Wait for container to be ready""" diff --git a/src/benchify/types/stack_create_and_run_response.py b/src/benchify/types/stack_create_and_run_response.py deleted file mode 100644 index e57227f3..00000000 --- a/src/benchify/types/stack_create_and_run_response.py +++ /dev/null @@ -1,17 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List - -from .._models import BaseModel - -__all__ = ["StackCreateAndRunResponse"] - - -class StackCreateAndRunResponse(BaseModel): - id: str - - command: List[str] - - image: str - - status: str diff --git a/src/benchify/types/stack_create_response.py b/src/benchify/types/stack_create_response.py index 7a5d55db..0428d783 100644 --- a/src/benchify/types/stack_create_response.py +++ b/src/benchify/types/stack_create_response.py @@ -11,6 +11,8 @@ class BuildStatus(BaseModel): + """Build status information""" + phase: Literal["pending", "running", "completed", "failed"] """Build phase states""" @@ -25,6 +27,8 @@ class BuildStatus(BaseModel): class Service(BaseModel): + """Information about a service in the stack""" + id: str """Service identifier""" @@ -45,6 +49,8 @@ class Service(BaseModel): class StackCreateResponse(BaseModel): + """Response after creating a new stack""" + id: str """Stack identifier""" diff --git a/src/benchify/types/stack_get_logs_response.py b/src/benchify/types/stack_get_logs_response.py index 307a2ec4..cb5f9ecc 100644 --- a/src/benchify/types/stack_get_logs_response.py +++ b/src/benchify/types/stack_get_logs_response.py @@ -11,6 +11,8 @@ class Service(BaseModel): + """Logs from a single service""" + id: str """Service ID""" @@ -28,6 +30,8 @@ class Service(BaseModel): class StackGetLogsResponse(BaseModel): + """Structured logs response from stack""" + id: str """Stack ID""" diff --git a/src/benchify/types/stack_retrieve_response.py b/src/benchify/types/stack_retrieve_response.py index 550ac830..b573630c 100644 --- a/src/benchify/types/stack_retrieve_response.py +++ b/src/benchify/types/stack_retrieve_response.py @@ -11,6 +11,8 @@ class StackRetrieveResponse(BaseModel): + """Stack status response""" + id: str """Stack identifier""" diff --git a/src/benchify/types/stack_update_response.py b/src/benchify/types/stack_update_response.py index fc377b20..b64d22d5 100644 --- a/src/benchify/types/stack_update_response.py +++ b/src/benchify/types/stack_update_response.py @@ -11,6 +11,8 @@ class StackUpdateResponse(BaseModel): + """Response after patching a stack""" + id: str """Stack identifier""" diff --git a/src/benchify/types/stacks/__init__.py b/src/benchify/types/stacks/__init__.py new file mode 100644 index 00000000..f8ee8b14 --- /dev/null +++ b/src/benchify/types/stacks/__init__.py @@ -0,0 +1,3 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations diff --git a/src/benchify/types/validate_template_validate_params.py b/src/benchify/types/validate_template_validate_params.py index ce422093..c84bf411 100644 --- a/src/benchify/types/validate_template_validate_params.py +++ b/src/benchify/types/validate_template_validate_params.py @@ -29,5 +29,7 @@ class ValidateTemplateValidateParams(TypedDict, total=False): class Meta(TypedDict, total=False): + """Meta information for the request""" + external_id: Optional[str] """Customer tracking identifier""" diff --git a/tests/api_resources/fix/test_standard.py b/tests/api_resources/fix/test_standard.py index 8bfcd8ee..1e667606 100644 --- a/tests/api_resources/fix/test_standard.py +++ b/tests/api_resources/fix/test_standard.py @@ -17,7 +17,7 @@ class TestStandard: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create(self, client: Benchify) -> None: standard = client.fix.standard.create( @@ -35,7 +35,7 @@ def test_method_create(self, client: Benchify) -> None: ) assert_matches_type(StandardCreateResponse, standard, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create_with_all_params(self, client: Benchify) -> None: standard = client.fix.standard.create( @@ -69,6 +69,7 @@ def test_method_create_with_all_params(self, client: Benchify) -> None: } }, bundle=True, + continuation_event_id="continuation_event_id", event_id="", fix_types=["css", "ui", "dependency", "types"], meta={"external_id": "external_id"}, @@ -77,7 +78,7 @@ def test_method_create_with_all_params(self, client: Benchify) -> None: ) assert_matches_type(StandardCreateResponse, standard, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_create(self, client: Benchify) -> None: response = client.fix.standard.with_raw_response.create( @@ -99,7 +100,7 @@ def test_raw_response_create(self, client: Benchify) -> None: standard = response.parse() assert_matches_type(StandardCreateResponse, standard, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_create(self, client: Benchify) -> None: with client.fix.standard.with_streaming_response.create( @@ -129,7 +130,7 @@ class TestAsyncStandard: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create(self, async_client: AsyncBenchify) -> None: standard = await async_client.fix.standard.create( @@ -147,7 +148,7 @@ async def test_method_create(self, async_client: AsyncBenchify) -> None: ) assert_matches_type(StandardCreateResponse, standard, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create_with_all_params(self, async_client: AsyncBenchify) -> None: standard = await async_client.fix.standard.create( @@ -181,6 +182,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBenchify) } }, bundle=True, + continuation_event_id="continuation_event_id", event_id="", fix_types=["css", "ui", "dependency", "types"], meta={"external_id": "external_id"}, @@ -189,7 +191,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBenchify) ) assert_matches_type(StandardCreateResponse, standard, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_create(self, async_client: AsyncBenchify) -> None: response = await async_client.fix.standard.with_raw_response.create( @@ -211,7 +213,7 @@ async def test_raw_response_create(self, async_client: AsyncBenchify) -> None: standard = await response.parse() assert_matches_type(StandardCreateResponse, standard, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_create(self, async_client: AsyncBenchify) -> None: async with async_client.fix.standard.with_streaming_response.create( diff --git a/tests/api_resources/test_fix.py b/tests/api_resources/test_fix.py index 1b996c31..21eb2072 100644 --- a/tests/api_resources/test_fix.py +++ b/tests/api_resources/test_fix.py @@ -17,7 +17,7 @@ class TestFix: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create_ai_fallback(self, client: Benchify) -> None: fix = client.fix.create_ai_fallback( @@ -28,11 +28,10 @@ def test_method_create_ai_fallback(self, client: Benchify) -> None: } ], remaining_diagnostics={}, - template_path="benchify/default-template", ) assert_matches_type(FixCreateAIFallbackResponse, fix, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create_ai_fallback_with_all_params(self, client: Benchify) -> None: fix = client.fix.create_ai_fallback( @@ -61,15 +60,16 @@ def test_method_create_ai_fallback_with_all_params(self, client: Benchify) -> No ] } }, - template_path="benchify/default-template", + continuation_event_id="continuation_event_id", event_id="", include_context=True, max_attempts=3, meta={"external_id": "external_id"}, + template_path="benchify/default-template", ) assert_matches_type(FixCreateAIFallbackResponse, fix, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_create_ai_fallback(self, client: Benchify) -> None: response = client.fix.with_raw_response.create_ai_fallback( @@ -80,7 +80,6 @@ def test_raw_response_create_ai_fallback(self, client: Benchify) -> None: } ], remaining_diagnostics={}, - template_path="benchify/default-template", ) assert response.is_closed is True @@ -88,7 +87,7 @@ def test_raw_response_create_ai_fallback(self, client: Benchify) -> None: fix = response.parse() assert_matches_type(FixCreateAIFallbackResponse, fix, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_create_ai_fallback(self, client: Benchify) -> None: with client.fix.with_streaming_response.create_ai_fallback( @@ -99,7 +98,6 @@ def test_streaming_response_create_ai_fallback(self, client: Benchify) -> None: } ], remaining_diagnostics={}, - template_path="benchify/default-template", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -115,7 +113,7 @@ class TestAsyncFix: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create_ai_fallback(self, async_client: AsyncBenchify) -> None: fix = await async_client.fix.create_ai_fallback( @@ -126,11 +124,10 @@ async def test_method_create_ai_fallback(self, async_client: AsyncBenchify) -> N } ], remaining_diagnostics={}, - template_path="benchify/default-template", ) assert_matches_type(FixCreateAIFallbackResponse, fix, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create_ai_fallback_with_all_params(self, async_client: AsyncBenchify) -> None: fix = await async_client.fix.create_ai_fallback( @@ -159,15 +156,16 @@ async def test_method_create_ai_fallback_with_all_params(self, async_client: Asy ] } }, - template_path="benchify/default-template", + continuation_event_id="continuation_event_id", event_id="", include_context=True, max_attempts=3, meta={"external_id": "external_id"}, + template_path="benchify/default-template", ) assert_matches_type(FixCreateAIFallbackResponse, fix, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_create_ai_fallback(self, async_client: AsyncBenchify) -> None: response = await async_client.fix.with_raw_response.create_ai_fallback( @@ -178,7 +176,6 @@ async def test_raw_response_create_ai_fallback(self, async_client: AsyncBenchify } ], remaining_diagnostics={}, - template_path="benchify/default-template", ) assert response.is_closed is True @@ -186,7 +183,7 @@ async def test_raw_response_create_ai_fallback(self, async_client: AsyncBenchify fix = await response.parse() assert_matches_type(FixCreateAIFallbackResponse, fix, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_create_ai_fallback(self, async_client: AsyncBenchify) -> None: async with async_client.fix.with_streaming_response.create_ai_fallback( @@ -197,7 +194,6 @@ async def test_streaming_response_create_ai_fallback(self, async_client: AsyncBe } ], remaining_diagnostics={}, - template_path="benchify/default-template", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/api_resources/test_fix_parsing_and_diagnose.py b/tests/api_resources/test_fix_parsing_and_diagnose.py index a1ba24b9..fa467af9 100644 --- a/tests/api_resources/test_fix_parsing_and_diagnose.py +++ b/tests/api_resources/test_fix_parsing_and_diagnose.py @@ -17,13 +17,13 @@ class TestFixParsingAndDiagnose: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_detect_issues(self, client: Benchify) -> None: fix_parsing_and_diagnose = client.fix_parsing_and_diagnose.detect_issues() assert_matches_type(FixParsingAndDiagnoseDetectIssuesResponse, fix_parsing_and_diagnose, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_detect_issues_with_all_params(self, client: Benchify) -> None: fix_parsing_and_diagnose = client.fix_parsing_and_diagnose.detect_issues( @@ -43,7 +43,7 @@ def test_method_detect_issues_with_all_params(self, client: Benchify) -> None: ) assert_matches_type(FixParsingAndDiagnoseDetectIssuesResponse, fix_parsing_and_diagnose, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_detect_issues(self, client: Benchify) -> None: response = client.fix_parsing_and_diagnose.with_raw_response.detect_issues() @@ -53,7 +53,7 @@ def test_raw_response_detect_issues(self, client: Benchify) -> None: fix_parsing_and_diagnose = response.parse() assert_matches_type(FixParsingAndDiagnoseDetectIssuesResponse, fix_parsing_and_diagnose, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_detect_issues(self, client: Benchify) -> None: with client.fix_parsing_and_diagnose.with_streaming_response.detect_issues() as response: @@ -71,13 +71,13 @@ class TestAsyncFixParsingAndDiagnose: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_detect_issues(self, async_client: AsyncBenchify) -> None: fix_parsing_and_diagnose = await async_client.fix_parsing_and_diagnose.detect_issues() assert_matches_type(FixParsingAndDiagnoseDetectIssuesResponse, fix_parsing_and_diagnose, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_detect_issues_with_all_params(self, async_client: AsyncBenchify) -> None: fix_parsing_and_diagnose = await async_client.fix_parsing_and_diagnose.detect_issues( @@ -97,7 +97,7 @@ async def test_method_detect_issues_with_all_params(self, async_client: AsyncBen ) assert_matches_type(FixParsingAndDiagnoseDetectIssuesResponse, fix_parsing_and_diagnose, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_detect_issues(self, async_client: AsyncBenchify) -> None: response = await async_client.fix_parsing_and_diagnose.with_raw_response.detect_issues() @@ -107,7 +107,7 @@ async def test_raw_response_detect_issues(self, async_client: AsyncBenchify) -> fix_parsing_and_diagnose = await response.parse() assert_matches_type(FixParsingAndDiagnoseDetectIssuesResponse, fix_parsing_and_diagnose, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_detect_issues(self, async_client: AsyncBenchify) -> None: async with async_client.fix_parsing_and_diagnose.with_streaming_response.detect_issues() as response: diff --git a/tests/api_resources/test_fix_string_literals.py b/tests/api_resources/test_fix_string_literals.py index 027e3c08..c3d1c2bc 100644 --- a/tests/api_resources/test_fix_string_literals.py +++ b/tests/api_resources/test_fix_string_literals.py @@ -17,7 +17,7 @@ class TestFixStringLiterals: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create(self, client: Benchify) -> None: fix_string_literal = client.fix_string_literals.create( @@ -28,7 +28,7 @@ def test_method_create(self, client: Benchify) -> None: ) assert_matches_type(FixStringLiteralCreateResponse, fix_string_literal, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create_with_all_params(self, client: Benchify) -> None: fix_string_literal = client.fix_string_literals.create( @@ -41,7 +41,7 @@ def test_method_create_with_all_params(self, client: Benchify) -> None: ) assert_matches_type(FixStringLiteralCreateResponse, fix_string_literal, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_create(self, client: Benchify) -> None: response = client.fix_string_literals.with_raw_response.create( @@ -56,7 +56,7 @@ def test_raw_response_create(self, client: Benchify) -> None: fix_string_literal = response.parse() assert_matches_type(FixStringLiteralCreateResponse, fix_string_literal, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_create(self, client: Benchify) -> None: with client.fix_string_literals.with_streaming_response.create( @@ -79,7 +79,7 @@ class TestAsyncFixStringLiterals: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create(self, async_client: AsyncBenchify) -> None: fix_string_literal = await async_client.fix_string_literals.create( @@ -90,7 +90,7 @@ async def test_method_create(self, async_client: AsyncBenchify) -> None: ) assert_matches_type(FixStringLiteralCreateResponse, fix_string_literal, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create_with_all_params(self, async_client: AsyncBenchify) -> None: fix_string_literal = await async_client.fix_string_literals.create( @@ -103,7 +103,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBenchify) ) assert_matches_type(FixStringLiteralCreateResponse, fix_string_literal, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_create(self, async_client: AsyncBenchify) -> None: response = await async_client.fix_string_literals.with_raw_response.create( @@ -118,7 +118,7 @@ async def test_raw_response_create(self, async_client: AsyncBenchify) -> None: fix_string_literal = await response.parse() assert_matches_type(FixStringLiteralCreateResponse, fix_string_literal, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_create(self, async_client: AsyncBenchify) -> None: async with async_client.fix_string_literals.with_streaming_response.create( diff --git a/tests/api_resources/test_fixer.py b/tests/api_resources/test_fixer.py index 41efee77..a352d598 100644 --- a/tests/api_resources/test_fixer.py +++ b/tests/api_resources/test_fixer.py @@ -17,13 +17,13 @@ class TestFixer: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_run(self, client: Benchify) -> None: fixer = client.fixer.run() assert_matches_type(FixerRunResponse, fixer, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_run_with_all_params(self, client: Benchify) -> None: fixer = client.fixer.run( @@ -49,7 +49,7 @@ def test_method_run_with_all_params(self, client: Benchify) -> None: ) assert_matches_type(FixerRunResponse, fixer, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_run(self, client: Benchify) -> None: response = client.fixer.with_raw_response.run() @@ -59,7 +59,7 @@ def test_raw_response_run(self, client: Benchify) -> None: fixer = response.parse() assert_matches_type(FixerRunResponse, fixer, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_run(self, client: Benchify) -> None: with client.fixer.with_streaming_response.run() as response: @@ -77,13 +77,13 @@ class TestAsyncFixer: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_run(self, async_client: AsyncBenchify) -> None: fixer = await async_client.fixer.run() assert_matches_type(FixerRunResponse, fixer, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_run_with_all_params(self, async_client: AsyncBenchify) -> None: fixer = await async_client.fixer.run( @@ -109,7 +109,7 @@ async def test_method_run_with_all_params(self, async_client: AsyncBenchify) -> ) assert_matches_type(FixerRunResponse, fixer, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_run(self, async_client: AsyncBenchify) -> None: response = await async_client.fixer.with_raw_response.run() @@ -119,7 +119,7 @@ async def test_raw_response_run(self, async_client: AsyncBenchify) -> None: fixer = await response.parse() assert_matches_type(FixerRunResponse, fixer, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_run(self, async_client: AsyncBenchify) -> None: async with async_client.fixer.with_streaming_response.run() as response: diff --git a/tests/api_resources/test_stacks.py b/tests/api_resources/test_stacks.py index 3886b8fc..cdcc4455 100644 --- a/tests/api_resources/test_stacks.py +++ b/tests/api_resources/test_stacks.py @@ -17,9 +17,9 @@ StackReadFileResponse, StackRetrieveResponse, StackWriteFileResponse, - StackCreateAndRunResponse, StackExecuteCommandResponse, StackGetNetworkInfoResponse, + StackBundleMultipartResponse, StackWaitForDevServerURLResponse, ) @@ -29,7 +29,7 @@ class TestStacks: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create(self, client: Benchify) -> None: stack = client.stacks.create( @@ -39,7 +39,7 @@ def test_method_create(self, client: Benchify) -> None: ) assert_matches_type(StackCreateResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create_with_all_params(self, client: Benchify) -> None: stack = client.stacks.create( @@ -51,7 +51,7 @@ def test_method_create_with_all_params(self, client: Benchify) -> None: ) assert_matches_type(StackCreateResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_create(self, client: Benchify) -> None: response = client.stacks.with_raw_response.create( @@ -65,7 +65,7 @@ def test_raw_response_create(self, client: Benchify) -> None: stack = response.parse() assert_matches_type(StackCreateResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_create(self, client: Benchify) -> None: with client.stacks.with_streaming_response.create( @@ -81,7 +81,7 @@ def test_streaming_response_create(self, client: Benchify) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_retrieve(self, client: Benchify) -> None: stack = client.stacks.retrieve( @@ -89,7 +89,7 @@ def test_method_retrieve(self, client: Benchify) -> None: ) assert_matches_type(StackRetrieveResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_retrieve(self, client: Benchify) -> None: response = client.stacks.with_raw_response.retrieve( @@ -101,7 +101,7 @@ def test_raw_response_retrieve(self, client: Benchify) -> None: stack = response.parse() assert_matches_type(StackRetrieveResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_retrieve(self, client: Benchify) -> None: with client.stacks.with_streaming_response.retrieve( @@ -115,7 +115,7 @@ def test_streaming_response_retrieve(self, client: Benchify) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_retrieve(self, client: Benchify) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -123,7 +123,7 @@ def test_path_params_retrieve(self, client: Benchify) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_update(self, client: Benchify) -> None: stack = client.stacks.update( @@ -132,7 +132,7 @@ def test_method_update(self, client: Benchify) -> None: ) assert_matches_type(StackUpdateResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_update_with_all_params(self, client: Benchify) -> None: stack = client.stacks.update( @@ -145,7 +145,7 @@ def test_method_update_with_all_params(self, client: Benchify) -> None: ) assert_matches_type(StackUpdateResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_update(self, client: Benchify) -> None: response = client.stacks.with_raw_response.update( @@ -158,7 +158,7 @@ def test_raw_response_update(self, client: Benchify) -> None: stack = response.parse() assert_matches_type(StackUpdateResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_update(self, client: Benchify) -> None: with client.stacks.with_streaming_response.update( @@ -173,7 +173,7 @@ def test_streaming_response_update(self, client: Benchify) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_update(self, client: Benchify) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -182,55 +182,44 @@ def test_path_params_update(self, client: Benchify) -> None: idempotency_key="key-12345678", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize - def test_method_create_and_run(self, client: Benchify) -> None: - stack = client.stacks.create_and_run( - command=["sleep", "3600"], - image="curlimages/curl:latest", + def test_method_bundle_multipart(self, client: Benchify) -> None: + stack = client.stacks.bundle_multipart( + manifest='{"entrypoint":"src/index.ts"}', + tarball=b"raw file contents", ) - assert_matches_type(StackCreateAndRunResponse, stack, path=["response"]) + assert_matches_type(StackBundleMultipartResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize - def test_method_create_and_run_with_all_params(self, client: Benchify) -> None: - stack = client.stacks.create_and_run( - command=["sleep", "3600"], - image="curlimages/curl:latest", - ttl_seconds=3600, - wait=False, - ) - assert_matches_type(StackCreateAndRunResponse, stack, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_create_and_run(self, client: Benchify) -> None: - response = client.stacks.with_raw_response.create_and_run( - command=["sleep", "3600"], - image="curlimages/curl:latest", + def test_raw_response_bundle_multipart(self, client: Benchify) -> None: + response = client.stacks.with_raw_response.bundle_multipart( + manifest='{"entrypoint":"src/index.ts"}', + tarball=b"raw file contents", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" stack = response.parse() - assert_matches_type(StackCreateAndRunResponse, stack, path=["response"]) + assert_matches_type(StackBundleMultipartResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize - def test_streaming_response_create_and_run(self, client: Benchify) -> None: - with client.stacks.with_streaming_response.create_and_run( - command=["sleep", "3600"], - image="curlimages/curl:latest", + def test_streaming_response_bundle_multipart(self, client: Benchify) -> None: + with client.stacks.with_streaming_response.bundle_multipart( + manifest='{"entrypoint":"src/index.ts"}', + tarball=b"raw file contents", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" stack = response.parse() - assert_matches_type(StackCreateAndRunResponse, stack, path=["response"]) + assert_matches_type(StackBundleMultipartResponse, stack, path=["response"]) assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_destroy(self, client: Benchify) -> None: stack = client.stacks.destroy( @@ -238,7 +227,7 @@ def test_method_destroy(self, client: Benchify) -> None: ) assert stack is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_destroy(self, client: Benchify) -> None: response = client.stacks.with_raw_response.destroy( @@ -250,7 +239,7 @@ def test_raw_response_destroy(self, client: Benchify) -> None: stack = response.parse() assert stack is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_destroy(self, client: Benchify) -> None: with client.stacks.with_streaming_response.destroy( @@ -264,7 +253,7 @@ def test_streaming_response_destroy(self, client: Benchify) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_destroy(self, client: Benchify) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -272,7 +261,7 @@ def test_path_params_destroy(self, client: Benchify) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_execute_command(self, client: Benchify) -> None: stack = client.stacks.execute_command( @@ -281,7 +270,7 @@ def test_method_execute_command(self, client: Benchify) -> None: ) assert_matches_type(StackExecuteCommandResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_execute_command(self, client: Benchify) -> None: response = client.stacks.with_raw_response.execute_command( @@ -294,7 +283,7 @@ def test_raw_response_execute_command(self, client: Benchify) -> None: stack = response.parse() assert_matches_type(StackExecuteCommandResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_execute_command(self, client: Benchify) -> None: with client.stacks.with_streaming_response.execute_command( @@ -309,7 +298,7 @@ def test_streaming_response_execute_command(self, client: Benchify) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_execute_command(self, client: Benchify) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -318,7 +307,7 @@ def test_path_params_execute_command(self, client: Benchify) -> None: command=["curl", "-s", "https://example.com"], ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_get_logs(self, client: Benchify) -> None: stack = client.stacks.get_logs( @@ -326,7 +315,7 @@ def test_method_get_logs(self, client: Benchify) -> None: ) assert_matches_type(StackGetLogsResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_get_logs_with_all_params(self, client: Benchify) -> None: stack = client.stacks.get_logs( @@ -335,7 +324,7 @@ def test_method_get_logs_with_all_params(self, client: Benchify) -> None: ) assert_matches_type(StackGetLogsResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_get_logs(self, client: Benchify) -> None: response = client.stacks.with_raw_response.get_logs( @@ -347,7 +336,7 @@ def test_raw_response_get_logs(self, client: Benchify) -> None: stack = response.parse() assert_matches_type(StackGetLogsResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_get_logs(self, client: Benchify) -> None: with client.stacks.with_streaming_response.get_logs( @@ -361,7 +350,7 @@ def test_streaming_response_get_logs(self, client: Benchify) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_get_logs(self, client: Benchify) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -369,7 +358,7 @@ def test_path_params_get_logs(self, client: Benchify) -> None: id="", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_get_network_info(self, client: Benchify) -> None: stack = client.stacks.get_network_info( @@ -377,7 +366,7 @@ def test_method_get_network_info(self, client: Benchify) -> None: ) assert_matches_type(StackGetNetworkInfoResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_get_network_info(self, client: Benchify) -> None: response = client.stacks.with_raw_response.get_network_info( @@ -389,7 +378,7 @@ def test_raw_response_get_network_info(self, client: Benchify) -> None: stack = response.parse() assert_matches_type(StackGetNetworkInfoResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_get_network_info(self, client: Benchify) -> None: with client.stacks.with_streaming_response.get_network_info( @@ -403,7 +392,7 @@ def test_streaming_response_get_network_info(self, client: Benchify) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_get_network_info(self, client: Benchify) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -411,7 +400,7 @@ def test_path_params_get_network_info(self, client: Benchify) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_read_file(self, client: Benchify) -> None: stack = client.stacks.read_file( @@ -420,7 +409,7 @@ def test_method_read_file(self, client: Benchify) -> None: ) assert_matches_type(StackReadFileResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_read_file(self, client: Benchify) -> None: response = client.stacks.with_raw_response.read_file( @@ -433,7 +422,7 @@ def test_raw_response_read_file(self, client: Benchify) -> None: stack = response.parse() assert_matches_type(StackReadFileResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_read_file(self, client: Benchify) -> None: with client.stacks.with_streaming_response.read_file( @@ -448,7 +437,7 @@ def test_streaming_response_read_file(self, client: Benchify) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_read_file(self, client: Benchify) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -457,7 +446,7 @@ def test_path_params_read_file(self, client: Benchify) -> None: path="/workspace/index.html", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_reset(self, client: Benchify) -> None: stack = client.stacks.reset( @@ -466,7 +455,7 @@ def test_method_reset(self, client: Benchify) -> None: ) assert_matches_type(StackResetResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_reset_with_all_params(self, client: Benchify) -> None: stack = client.stacks.reset( @@ -476,7 +465,7 @@ def test_method_reset_with_all_params(self, client: Benchify) -> None: ) assert_matches_type(StackResetResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_reset(self, client: Benchify) -> None: response = client.stacks.with_raw_response.reset( @@ -489,7 +478,7 @@ def test_raw_response_reset(self, client: Benchify) -> None: stack = response.parse() assert_matches_type(StackResetResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_reset(self, client: Benchify) -> None: with client.stacks.with_streaming_response.reset( @@ -504,7 +493,7 @@ def test_streaming_response_reset(self, client: Benchify) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_reset(self, client: Benchify) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -513,7 +502,7 @@ def test_path_params_reset(self, client: Benchify) -> None: tarball_base64="tarball_base64", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_wait_for_dev_server_url(self, client: Benchify) -> None: stack = client.stacks.wait_for_dev_server_url( @@ -521,7 +510,7 @@ def test_method_wait_for_dev_server_url(self, client: Benchify) -> None: ) assert_matches_type(StackWaitForDevServerURLResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_wait_for_dev_server_url_with_all_params(self, client: Benchify) -> None: stack = client.stacks.wait_for_dev_server_url( @@ -531,7 +520,7 @@ def test_method_wait_for_dev_server_url_with_all_params(self, client: Benchify) ) assert_matches_type(StackWaitForDevServerURLResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_wait_for_dev_server_url(self, client: Benchify) -> None: response = client.stacks.with_raw_response.wait_for_dev_server_url( @@ -543,7 +532,7 @@ def test_raw_response_wait_for_dev_server_url(self, client: Benchify) -> None: stack = response.parse() assert_matches_type(StackWaitForDevServerURLResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_wait_for_dev_server_url(self, client: Benchify) -> None: with client.stacks.with_streaming_response.wait_for_dev_server_url( @@ -557,7 +546,7 @@ def test_streaming_response_wait_for_dev_server_url(self, client: Benchify) -> N assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_wait_for_dev_server_url(self, client: Benchify) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -565,7 +554,7 @@ def test_path_params_wait_for_dev_server_url(self, client: Benchify) -> None: id="", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_write_file(self, client: Benchify) -> None: stack = client.stacks.write_file( @@ -575,7 +564,7 @@ def test_method_write_file(self, client: Benchify) -> None: ) assert_matches_type(StackWriteFileResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_write_file(self, client: Benchify) -> None: response = client.stacks.with_raw_response.write_file( @@ -589,7 +578,7 @@ def test_raw_response_write_file(self, client: Benchify) -> None: stack = response.parse() assert_matches_type(StackWriteFileResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_write_file(self, client: Benchify) -> None: with client.stacks.with_streaming_response.write_file( @@ -605,7 +594,7 @@ def test_streaming_response_write_file(self, client: Benchify) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_write_file(self, client: Benchify) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -621,7 +610,7 @@ class TestAsyncStacks: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create(self, async_client: AsyncBenchify) -> None: stack = await async_client.stacks.create( @@ -631,7 +620,7 @@ async def test_method_create(self, async_client: AsyncBenchify) -> None: ) assert_matches_type(StackCreateResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create_with_all_params(self, async_client: AsyncBenchify) -> None: stack = await async_client.stacks.create( @@ -643,7 +632,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBenchify) ) assert_matches_type(StackCreateResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_create(self, async_client: AsyncBenchify) -> None: response = await async_client.stacks.with_raw_response.create( @@ -657,7 +646,7 @@ async def test_raw_response_create(self, async_client: AsyncBenchify) -> None: stack = await response.parse() assert_matches_type(StackCreateResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_create(self, async_client: AsyncBenchify) -> None: async with async_client.stacks.with_streaming_response.create( @@ -673,7 +662,7 @@ async def test_streaming_response_create(self, async_client: AsyncBenchify) -> N assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_retrieve(self, async_client: AsyncBenchify) -> None: stack = await async_client.stacks.retrieve( @@ -681,7 +670,7 @@ async def test_method_retrieve(self, async_client: AsyncBenchify) -> None: ) assert_matches_type(StackRetrieveResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_retrieve(self, async_client: AsyncBenchify) -> None: response = await async_client.stacks.with_raw_response.retrieve( @@ -693,7 +682,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncBenchify) -> None: stack = await response.parse() assert_matches_type(StackRetrieveResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncBenchify) -> None: async with async_client.stacks.with_streaming_response.retrieve( @@ -707,7 +696,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncBenchify) -> assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_retrieve(self, async_client: AsyncBenchify) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -715,7 +704,7 @@ async def test_path_params_retrieve(self, async_client: AsyncBenchify) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_update(self, async_client: AsyncBenchify) -> None: stack = await async_client.stacks.update( @@ -724,7 +713,7 @@ async def test_method_update(self, async_client: AsyncBenchify) -> None: ) assert_matches_type(StackUpdateResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_update_with_all_params(self, async_client: AsyncBenchify) -> None: stack = await async_client.stacks.update( @@ -737,7 +726,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncBenchify) ) assert_matches_type(StackUpdateResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_update(self, async_client: AsyncBenchify) -> None: response = await async_client.stacks.with_raw_response.update( @@ -750,7 +739,7 @@ async def test_raw_response_update(self, async_client: AsyncBenchify) -> None: stack = await response.parse() assert_matches_type(StackUpdateResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_update(self, async_client: AsyncBenchify) -> None: async with async_client.stacks.with_streaming_response.update( @@ -765,7 +754,7 @@ async def test_streaming_response_update(self, async_client: AsyncBenchify) -> N assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_update(self, async_client: AsyncBenchify) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -774,55 +763,44 @@ async def test_path_params_update(self, async_client: AsyncBenchify) -> None: idempotency_key="key-12345678", ) - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_create_and_run(self, async_client: AsyncBenchify) -> None: - stack = await async_client.stacks.create_and_run( - command=["sleep", "3600"], - image="curlimages/curl:latest", - ) - assert_matches_type(StackCreateAndRunResponse, stack, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize - async def test_method_create_and_run_with_all_params(self, async_client: AsyncBenchify) -> None: - stack = await async_client.stacks.create_and_run( - command=["sleep", "3600"], - image="curlimages/curl:latest", - ttl_seconds=3600, - wait=False, + async def test_method_bundle_multipart(self, async_client: AsyncBenchify) -> None: + stack = await async_client.stacks.bundle_multipart( + manifest='{"entrypoint":"src/index.ts"}', + tarball=b"raw file contents", ) - assert_matches_type(StackCreateAndRunResponse, stack, path=["response"]) + assert_matches_type(StackBundleMultipartResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize - async def test_raw_response_create_and_run(self, async_client: AsyncBenchify) -> None: - response = await async_client.stacks.with_raw_response.create_and_run( - command=["sleep", "3600"], - image="curlimages/curl:latest", + async def test_raw_response_bundle_multipart(self, async_client: AsyncBenchify) -> None: + response = await async_client.stacks.with_raw_response.bundle_multipart( + manifest='{"entrypoint":"src/index.ts"}', + tarball=b"raw file contents", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" stack = await response.parse() - assert_matches_type(StackCreateAndRunResponse, stack, path=["response"]) + assert_matches_type(StackBundleMultipartResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize - async def test_streaming_response_create_and_run(self, async_client: AsyncBenchify) -> None: - async with async_client.stacks.with_streaming_response.create_and_run( - command=["sleep", "3600"], - image="curlimages/curl:latest", + async def test_streaming_response_bundle_multipart(self, async_client: AsyncBenchify) -> None: + async with async_client.stacks.with_streaming_response.bundle_multipart( + manifest='{"entrypoint":"src/index.ts"}', + tarball=b"raw file contents", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" stack = await response.parse() - assert_matches_type(StackCreateAndRunResponse, stack, path=["response"]) + assert_matches_type(StackBundleMultipartResponse, stack, path=["response"]) assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_destroy(self, async_client: AsyncBenchify) -> None: stack = await async_client.stacks.destroy( @@ -830,7 +808,7 @@ async def test_method_destroy(self, async_client: AsyncBenchify) -> None: ) assert stack is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_destroy(self, async_client: AsyncBenchify) -> None: response = await async_client.stacks.with_raw_response.destroy( @@ -842,7 +820,7 @@ async def test_raw_response_destroy(self, async_client: AsyncBenchify) -> None: stack = await response.parse() assert stack is None - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_destroy(self, async_client: AsyncBenchify) -> None: async with async_client.stacks.with_streaming_response.destroy( @@ -856,7 +834,7 @@ async def test_streaming_response_destroy(self, async_client: AsyncBenchify) -> assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_destroy(self, async_client: AsyncBenchify) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -864,7 +842,7 @@ async def test_path_params_destroy(self, async_client: AsyncBenchify) -> None: "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_execute_command(self, async_client: AsyncBenchify) -> None: stack = await async_client.stacks.execute_command( @@ -873,7 +851,7 @@ async def test_method_execute_command(self, async_client: AsyncBenchify) -> None ) assert_matches_type(StackExecuteCommandResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_execute_command(self, async_client: AsyncBenchify) -> None: response = await async_client.stacks.with_raw_response.execute_command( @@ -886,7 +864,7 @@ async def test_raw_response_execute_command(self, async_client: AsyncBenchify) - stack = await response.parse() assert_matches_type(StackExecuteCommandResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_execute_command(self, async_client: AsyncBenchify) -> None: async with async_client.stacks.with_streaming_response.execute_command( @@ -901,7 +879,7 @@ async def test_streaming_response_execute_command(self, async_client: AsyncBench assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_execute_command(self, async_client: AsyncBenchify) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -910,7 +888,7 @@ async def test_path_params_execute_command(self, async_client: AsyncBenchify) -> command=["curl", "-s", "https://example.com"], ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_get_logs(self, async_client: AsyncBenchify) -> None: stack = await async_client.stacks.get_logs( @@ -918,7 +896,7 @@ async def test_method_get_logs(self, async_client: AsyncBenchify) -> None: ) assert_matches_type(StackGetLogsResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_get_logs_with_all_params(self, async_client: AsyncBenchify) -> None: stack = await async_client.stacks.get_logs( @@ -927,7 +905,7 @@ async def test_method_get_logs_with_all_params(self, async_client: AsyncBenchify ) assert_matches_type(StackGetLogsResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_get_logs(self, async_client: AsyncBenchify) -> None: response = await async_client.stacks.with_raw_response.get_logs( @@ -939,7 +917,7 @@ async def test_raw_response_get_logs(self, async_client: AsyncBenchify) -> None: stack = await response.parse() assert_matches_type(StackGetLogsResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_get_logs(self, async_client: AsyncBenchify) -> None: async with async_client.stacks.with_streaming_response.get_logs( @@ -953,7 +931,7 @@ async def test_streaming_response_get_logs(self, async_client: AsyncBenchify) -> assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_get_logs(self, async_client: AsyncBenchify) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -961,7 +939,7 @@ async def test_path_params_get_logs(self, async_client: AsyncBenchify) -> None: id="", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_get_network_info(self, async_client: AsyncBenchify) -> None: stack = await async_client.stacks.get_network_info( @@ -969,7 +947,7 @@ async def test_method_get_network_info(self, async_client: AsyncBenchify) -> Non ) assert_matches_type(StackGetNetworkInfoResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_get_network_info(self, async_client: AsyncBenchify) -> None: response = await async_client.stacks.with_raw_response.get_network_info( @@ -981,7 +959,7 @@ async def test_raw_response_get_network_info(self, async_client: AsyncBenchify) stack = await response.parse() assert_matches_type(StackGetNetworkInfoResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_get_network_info(self, async_client: AsyncBenchify) -> None: async with async_client.stacks.with_streaming_response.get_network_info( @@ -995,7 +973,7 @@ async def test_streaming_response_get_network_info(self, async_client: AsyncBenc assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_get_network_info(self, async_client: AsyncBenchify) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -1003,7 +981,7 @@ async def test_path_params_get_network_info(self, async_client: AsyncBenchify) - "", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_read_file(self, async_client: AsyncBenchify) -> None: stack = await async_client.stacks.read_file( @@ -1012,7 +990,7 @@ async def test_method_read_file(self, async_client: AsyncBenchify) -> None: ) assert_matches_type(StackReadFileResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_read_file(self, async_client: AsyncBenchify) -> None: response = await async_client.stacks.with_raw_response.read_file( @@ -1025,7 +1003,7 @@ async def test_raw_response_read_file(self, async_client: AsyncBenchify) -> None stack = await response.parse() assert_matches_type(StackReadFileResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_read_file(self, async_client: AsyncBenchify) -> None: async with async_client.stacks.with_streaming_response.read_file( @@ -1040,7 +1018,7 @@ async def test_streaming_response_read_file(self, async_client: AsyncBenchify) - assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_read_file(self, async_client: AsyncBenchify) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -1049,7 +1027,7 @@ async def test_path_params_read_file(self, async_client: AsyncBenchify) -> None: path="/workspace/index.html", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_reset(self, async_client: AsyncBenchify) -> None: stack = await async_client.stacks.reset( @@ -1058,7 +1036,7 @@ async def test_method_reset(self, async_client: AsyncBenchify) -> None: ) assert_matches_type(StackResetResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_reset_with_all_params(self, async_client: AsyncBenchify) -> None: stack = await async_client.stacks.reset( @@ -1068,7 +1046,7 @@ async def test_method_reset_with_all_params(self, async_client: AsyncBenchify) - ) assert_matches_type(StackResetResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_reset(self, async_client: AsyncBenchify) -> None: response = await async_client.stacks.with_raw_response.reset( @@ -1081,7 +1059,7 @@ async def test_raw_response_reset(self, async_client: AsyncBenchify) -> None: stack = await response.parse() assert_matches_type(StackResetResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_reset(self, async_client: AsyncBenchify) -> None: async with async_client.stacks.with_streaming_response.reset( @@ -1096,7 +1074,7 @@ async def test_streaming_response_reset(self, async_client: AsyncBenchify) -> No assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_reset(self, async_client: AsyncBenchify) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -1105,7 +1083,7 @@ async def test_path_params_reset(self, async_client: AsyncBenchify) -> None: tarball_base64="tarball_base64", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_wait_for_dev_server_url(self, async_client: AsyncBenchify) -> None: stack = await async_client.stacks.wait_for_dev_server_url( @@ -1113,7 +1091,7 @@ async def test_method_wait_for_dev_server_url(self, async_client: AsyncBenchify) ) assert_matches_type(StackWaitForDevServerURLResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_wait_for_dev_server_url_with_all_params(self, async_client: AsyncBenchify) -> None: stack = await async_client.stacks.wait_for_dev_server_url( @@ -1123,7 +1101,7 @@ async def test_method_wait_for_dev_server_url_with_all_params(self, async_client ) assert_matches_type(StackWaitForDevServerURLResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_wait_for_dev_server_url(self, async_client: AsyncBenchify) -> None: response = await async_client.stacks.with_raw_response.wait_for_dev_server_url( @@ -1135,7 +1113,7 @@ async def test_raw_response_wait_for_dev_server_url(self, async_client: AsyncBen stack = await response.parse() assert_matches_type(StackWaitForDevServerURLResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_wait_for_dev_server_url(self, async_client: AsyncBenchify) -> None: async with async_client.stacks.with_streaming_response.wait_for_dev_server_url( @@ -1149,7 +1127,7 @@ async def test_streaming_response_wait_for_dev_server_url(self, async_client: As assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_wait_for_dev_server_url(self, async_client: AsyncBenchify) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): @@ -1157,7 +1135,7 @@ async def test_path_params_wait_for_dev_server_url(self, async_client: AsyncBenc id="", ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_write_file(self, async_client: AsyncBenchify) -> None: stack = await async_client.stacks.write_file( @@ -1167,7 +1145,7 @@ async def test_method_write_file(self, async_client: AsyncBenchify) -> None: ) assert_matches_type(StackWriteFileResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_write_file(self, async_client: AsyncBenchify) -> None: response = await async_client.stacks.with_raw_response.write_file( @@ -1181,7 +1159,7 @@ async def test_raw_response_write_file(self, async_client: AsyncBenchify) -> Non stack = await response.parse() assert_matches_type(StackWriteFileResponse, stack, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_write_file(self, async_client: AsyncBenchify) -> None: async with async_client.stacks.with_streaming_response.write_file( @@ -1197,7 +1175,7 @@ async def test_streaming_response_write_file(self, async_client: AsyncBenchify) assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_write_file(self, async_client: AsyncBenchify) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): diff --git a/tests/api_resources/test_validate_template.py b/tests/api_resources/test_validate_template.py index f0e2c3fb..89d783cb 100644 --- a/tests/api_resources/test_validate_template.py +++ b/tests/api_resources/test_validate_template.py @@ -17,13 +17,13 @@ class TestValidateTemplate: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_validate(self, client: Benchify) -> None: validate_template = client.validate_template.validate() assert_matches_type(ValidateTemplateValidateResponse, validate_template, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_validate_with_all_params(self, client: Benchify) -> None: validate_template = client.validate_template.validate( @@ -36,7 +36,7 @@ def test_method_validate_with_all_params(self, client: Benchify) -> None: ) assert_matches_type(ValidateTemplateValidateResponse, validate_template, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_validate(self, client: Benchify) -> None: response = client.validate_template.with_raw_response.validate() @@ -46,7 +46,7 @@ def test_raw_response_validate(self, client: Benchify) -> None: validate_template = response.parse() assert_matches_type(ValidateTemplateValidateResponse, validate_template, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_validate(self, client: Benchify) -> None: with client.validate_template.with_streaming_response.validate() as response: @@ -64,13 +64,13 @@ class TestAsyncValidateTemplate: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_validate(self, async_client: AsyncBenchify) -> None: validate_template = await async_client.validate_template.validate() assert_matches_type(ValidateTemplateValidateResponse, validate_template, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_validate_with_all_params(self, async_client: AsyncBenchify) -> None: validate_template = await async_client.validate_template.validate( @@ -83,7 +83,7 @@ async def test_method_validate_with_all_params(self, async_client: AsyncBenchify ) assert_matches_type(ValidateTemplateValidateResponse, validate_template, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_validate(self, async_client: AsyncBenchify) -> None: response = await async_client.validate_template.with_raw_response.validate() @@ -93,7 +93,7 @@ async def test_raw_response_validate(self, async_client: AsyncBenchify) -> None: validate_template = await response.parse() assert_matches_type(ValidateTemplateValidateResponse, validate_template, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_validate(self, async_client: AsyncBenchify) -> None: async with async_client.validate_template.with_streaming_response.validate() as response: diff --git a/tests/test_client.py b/tests/test_client.py index 4e256108..bba541d5 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -8,10 +8,11 @@ import json import asyncio import inspect +import dataclasses import tracemalloc -from typing import Any, Union, cast +from typing import Any, Union, TypeVar, Callable, Iterable, Iterator, Optional, Coroutine, cast from unittest import mock -from typing_extensions import Literal +from typing_extensions import Literal, AsyncIterator, override import httpx import pytest @@ -36,6 +37,7 @@ from .utils import update_env +T = TypeVar("T") base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") api_key = "My API Key" @@ -50,6 +52,57 @@ def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float: return 0.1 +def mirror_request_content(request: httpx.Request) -> httpx.Response: + return httpx.Response(200, content=request.content) + + +# note: we can't use the httpx.MockTransport class as it consumes the request +# body itself, which means we can't test that the body is read lazily +class MockTransport(httpx.BaseTransport, httpx.AsyncBaseTransport): + def __init__( + self, + handler: Callable[[httpx.Request], httpx.Response] + | Callable[[httpx.Request], Coroutine[Any, Any, httpx.Response]], + ) -> None: + self.handler = handler + + @override + def handle_request( + self, + request: httpx.Request, + ) -> httpx.Response: + assert not inspect.iscoroutinefunction(self.handler), "handler must not be a coroutine function" + assert inspect.isfunction(self.handler), "handler must be a function" + return self.handler(request) + + @override + async def handle_async_request( + self, + request: httpx.Request, + ) -> httpx.Response: + assert inspect.iscoroutinefunction(self.handler), "handler must be a coroutine function" + return await self.handler(request) + + +@dataclasses.dataclass +class Counter: + value: int = 0 + + +def _make_sync_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> Iterator[T]: + for item in iterable: + if counter: + counter.value += 1 + yield item + + +async def _make_async_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> AsyncIterator[T]: + for item in iterable: + if counter: + counter.value += 1 + yield item + + def _get_open_connections(client: Benchify | AsyncBenchify) -> int: transport = client._client._transport assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) @@ -511,6 +564,70 @@ def test_multipart_repeating_array(self, client: Benchify) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload(self, respx_mock: MockRouter, client: Benchify) -> 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 Benchify( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.Client(transport=MockTransport(handler=mock_handler)), + ) as client: + response = client.post( + "/upload", + content=iterator, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + assert counter.value == 1 + + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload_with_body_is_deprecated(self, respx_mock: MockRouter, client: Benchify) -> 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: Benchify) -> None: class Model1(BaseModel): @@ -839,6 +956,14 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + # Delete in case our environment has 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() @@ -1339,6 +1464,72 @@ def test_multipart_repeating_array(self, async_client: AsyncBenchify) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload(self, respx_mock: MockRouter, async_client: AsyncBenchify) -> 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 AsyncBenchify( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.AsyncClient(transport=MockTransport(handler=mock_handler)), + ) as client: + response = await client.post( + "/upload", + content=iterator, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + assert counter.value == 1 + + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload_with_body_is_deprecated( + self, respx_mock: MockRouter, async_client: AsyncBenchify + ) -> 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: AsyncBenchify) -> None: class Model1(BaseModel): @@ -1688,6 +1879,14 @@ async def test_get_platform(self) -> None: async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + # Delete in case our environment has 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() diff --git a/tests/test_models.py b/tests/test_models.py index 13cd5a99..8e5b0837 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -9,7 +9,7 @@ from benchify._utils import PropertyInfo from benchify._compat import PYDANTIC_V1, parse_obj, model_dump, model_json -from benchify._models import BaseModel, construct_type +from benchify._models import DISCRIMINATOR_CACHE, BaseModel, construct_type class BasicModel(BaseModel): @@ -809,7 +809,7 @@ class B(BaseModel): UnionType = cast(Any, Union[A, B]) - assert not hasattr(UnionType, "__discriminator__") + assert not DISCRIMINATOR_CACHE.get(UnionType) m = construct_type( value={"type": "b", "data": "foo"}, type_=cast(Any, Annotated[UnionType, PropertyInfo(discriminator="type")]) @@ -818,7 +818,7 @@ class B(BaseModel): assert m.type == "b" assert m.data == "foo" # type: ignore[comparison-overlap] - discriminator = UnionType.__discriminator__ + discriminator = DISCRIMINATOR_CACHE.get(UnionType) assert discriminator is not None m = construct_type( @@ -830,7 +830,7 @@ class B(BaseModel): # if the discriminator details object stays the same between invocations then # we hit the cache - assert UnionType.__discriminator__ is discriminator + assert DISCRIMINATOR_CACHE.get(UnionType) is discriminator @pytest.mark.skipif(PYDANTIC_V1, reason="TypeAliasType is not supported in Pydantic v1") diff --git a/tests/test_utils/test_json.py b/tests/test_utils/test_json.py new file mode 100644 index 00000000..687daa41 --- /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 benchify import _compat +from benchify._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}}'